T newInstance(final String clazzOrShortcut, String type, final Settings settings, final Path configPath) {
+
+ String clazz = clazzOrShortcut;
+ boolean isEnterprise = false;
+
+ if(authImplMap.containsKey(clazz+"_"+type)) {
+ clazz = authImplMap.get(clazz+"_"+type);
+ } else {
+ isEnterprise = true;
+ }
+
+ if(ReflectionHelper.isEnterpriseAAAModule(clazz)) {
+ isEnterprise = true;
+ }
+
+ return ReflectionHelper.instantiateAAA(clazz, settings, configPath, isEnterprise);
+ }
+
+ private void destroyDestroyables() {
+ for (Destroyable destroyable : this.destroyableComponents) {
+ try {
+ destroyable.destroy();
+ } catch (Exception e) {
+ log.error("Error while destroying " + destroyable, e);
+ }
+ }
+
+ this.destroyableComponents.clear();
+ }
+
+ private User resolveTransportUsernameAttribute(User pkiUser) {
+ //#547
+ if(transportUsernameAttribute != null && !transportUsernameAttribute.isEmpty()) {
+ try {
+ final LdapName sslPrincipalAsLdapName = new LdapName(pkiUser.getName());
+ for(final Rdn rdn: sslPrincipalAsLdapName.getRdns()) {
+ if(rdn.getType().equals(transportUsernameAttribute)) {
+ return new User((String) rdn.getValue());
+ }
+ }
+ } catch (InvalidNameException e) {
+ //cannot happen
+ }
+ }
+
+ return pkiUser;
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java
new file mode 100644
index 0000000000..780e60b810
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/Destroyable.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth;
+
+public interface Destroyable {
+ void destroy();
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java
new file mode 100644
index 0000000000..937a82746d
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/HTTPAuthenticator.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth;
+
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestRequest;
+
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+
+/**
+ * Open Distro Security custom HTTP authenticators need to implement this interface.
+ *
+ * A HTTP authenticator extracts {@link AuthCredentials} from a {@link RestRequest}
+ *
+ *
+ * Implementation classes must provide a public constructor
+ *
+ * {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)}
+ *
+ * The constructor should not throw any exception in case of an initialization problem.
+ * Instead catch all exceptions and log a appropriate error message. A logger can be instantiated like:
+ *
+ * {@code private final Logger log = LogManager.getLogger(this.getClass());}
+ *
+ */
+public interface HTTPAuthenticator {
+
+ /**
+ * The type (name) of the authenticator. Only for logging.
+ * @return the type
+ */
+ String getType();
+
+ /**
+ * Extract {@link AuthCredentials} from {@link RestRequest}
+ *
+ * @param request The rest request
+ * @param context The current thread context
+ * @return The authentication credentials (complete or incomplete) or null when no credentials are found in the request
+ *
+ * When the credentials could be fully extracted from the request {@code .markComplete()} must be called on the {@link AuthCredentials} which are returned.
+ * If the authentication flow needs another roundtrip with the request originator do not mark it as complete.
+ * @throws ElasticsearchSecurityException
+ */
+ AuthCredentials extractCredentials(RestRequest request, ThreadContext context) throws ElasticsearchSecurityException;
+
+ /**
+ * If the {@code extractCredentials()} call was not successful or the authentication flow needs another roundtrip this method
+ * will be called. If the custom HTTP authenticator does not support this method is a no-op and false should be returned.
+ *
+ * If the custom HTTP authenticator does support re-request authentication or supports authentication flows with multiple roundtrips
+ * then the response should be sent (through the channel) and true must be returned.
+ *
+ * @param channel The rest channel to sent back the response via {@code channel.sendResponse()}
+ * @param credentials The credentials from the prior authentication attempt
+ * @return false if re-request is not supported/necessary, true otherwise.
+ * If true is returned {@code channel.sendResponse()} must be called so that the request completes.
+ */
+ boolean reRequestAuthentication(final RestChannel channel, AuthCredentials credentials);
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java
new file mode 100644
index 0000000000..76ec21f76d
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/UserInjector.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Map;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.TransportAddress;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog;
+import com.amazon.opendistroforelasticsearch.security.http.XFFResolver;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityUtils;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+import com.google.common.base.Strings;
+
+public class UserInjector {
+
+ protected final Logger log = LogManager.getLogger(UserInjector.class);
+
+ private final ThreadPool threadPool;
+ private final AuditLog auditLog;
+ private final XFFResolver xffResolver;
+ private final Boolean injectUserEnabled;
+
+ UserInjector(Settings settings, ThreadPool threadPool, AuditLog auditLog, XFFResolver xffResolver) {
+ this.threadPool = threadPool;
+ this.auditLog = auditLog;
+ this.xffResolver = xffResolver;
+ this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false);
+
+ }
+
+ boolean injectUser(RestRequest request) {
+
+ if (!injectUserEnabled) {
+ return false;
+ }
+
+ String injectedUserString = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER);
+
+ if (log.isDebugEnabled()) {
+ log.debug("Injected user string: {}", injectedUserString);
+ }
+
+ if (Strings.isNullOrEmpty(injectedUserString)) {
+ return false;
+ }
+ // username|role1,role2|remoteIP:port|attributeKey,attributeValue,attributeKey,attributeValue, ...|requestedTenant
+ String[] parts = injectedUserString.split("\\|");
+
+ if (parts.length == 0) {
+ log.error("User string malformed, could not extract parts. User string was '{}.' User injection failed.", injectedUserString);
+ return false;
+ }
+
+ // username
+ if (Strings.isNullOrEmpty(parts[0])) {
+ log.error("Username must not be null, user string was '{}.' User injection failed.", injectedUserString);
+ return false;
+ }
+
+ final User user = new User(parts[0]);
+
+ // backend roles
+ if (parts.length > 1 && !Strings.isNullOrEmpty(parts[1])) {
+ if (parts[1].length() > 0) {
+ user.addRoles(Arrays.asList(parts[1].split(",")));
+ }
+ }
+
+ // custom attributes
+ if (parts.length > 3 && !Strings.isNullOrEmpty(parts[3])) {
+ Map attributes = OpenDistroSecurityUtils.mapFromArray((parts[3].split(",")));
+ if (attributes == null) {
+ log.error("Could not parse custom attributes {}, user injection failed.", parts[3]);
+ return false;
+ } else {
+ user.addAttributes(attributes);
+ }
+ }
+
+ // requested tenant
+ if (parts.length > 4 && !Strings.isNullOrEmpty(parts[4])) {
+ user.setRequestedTenant(parts[4]);
+ }
+
+ // remote IP - we can set it only once, so we do it last. If non is given,
+ // BackendRegistry/XFFResolver will do the job
+ if (parts.length > 2 && !Strings.isNullOrEmpty(parts[2])) {
+ // format is ip:port
+ String[] ipAndPort = parts[2].split(":");
+ if (ipAndPort.length != 2) {
+ log.error("Remote address must have format ip:port, was: {}. User injection failed.", parts[2]);
+ return false;
+ } else {
+ try {
+ InetAddress iAdress = InetAddress.getByName(ipAndPort[0]);
+ int port = Integer.parseInt(ipAndPort[1]);
+ threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, new TransportAddress(iAdress, port));
+ } catch (UnknownHostException | NumberFormatException e) {
+ log.error("Cannot parse remote IP or port: {}, user injection failed.", parts[2], e);
+ return false;
+ }
+ }
+ } else {
+ threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, xffResolver.resolve(request));
+ }
+
+ // mark user injected for proper admin handling
+ user.setInjected(true);
+
+ threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user);
+ auditLog.logSucceededLogin(parts[0], true, null, request);
+ if (log.isTraceEnabled()) {
+ log.trace("Injected user object:{} ", user.toString());
+ }
+ return true;
+
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java
new file mode 100644
index 0000000000..ffb83a2d34
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/InternalAuthenticationBackend.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.common.settings.Settings;
+
+import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend;
+import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend;
+import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationRepository;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class InternalAuthenticationBackend implements AuthenticationBackend, AuthorizationBackend {
+
+ private final ConfigurationRepository configurationRepository;
+
+ public InternalAuthenticationBackend(final ConfigurationRepository configurationRepository) {
+ super();
+ this.configurationRepository = configurationRepository;
+ }
+
+ @Override
+ public boolean exists(User user) {
+
+ final Settings cfg = getConfigSettings();
+ if (cfg == null) {
+ return false;
+ }
+
+ String hashed = cfg.get(user.getName() + ".hash");
+
+ if (hashed == null) {
+
+ for(String username:cfg.names()) {
+ String u = cfg.get(username + ".username");
+ if(user.getName().equals(u)) {
+ hashed = cfg.get(username+ ".hash");
+ break;
+ }
+ }
+
+ if(hashed == null) {
+ return false;
+ }
+ }
+
+ final List roles = cfg.getAsList(user.getName() + ".roles", Collections.emptyList());
+
+ if(roles != null) {
+ user.addRoles(roles);
+ }
+
+ return true;
+ }
+
+ @Override
+ public User authenticate(final AuthCredentials credentials) {
+
+ final Settings cfg = getConfigSettings();
+ if (cfg == null) {
+ throw new ElasticsearchSecurityException("Internal authentication backend not configured. May be Open Distro Security is not initialized");
+
+ }
+
+ String hashed = cfg.get(credentials.getUsername() + ".hash");
+
+ if (hashed == null) {
+
+ for(String username:cfg.names()) {
+ String u = cfg.get(username + ".username");
+ if(credentials.getUsername().equals(u)) {
+ hashed = cfg.get(username+ ".hash");
+ break;
+ }
+ }
+
+ if(hashed == null) {
+ throw new ElasticsearchSecurityException(credentials.getUsername() + " not found");
+ }
+ }
+
+ final byte[] password = credentials.getPassword();
+
+ if(password == null || password.length == 0) {
+ throw new ElasticsearchSecurityException("empty passwords not supported");
+ }
+
+ ByteBuffer wrap = ByteBuffer.wrap(password);
+ CharBuffer buf = StandardCharsets.UTF_8.decode(wrap);
+ char[] array = new char[buf.limit()];
+ buf.get(array);
+
+ Arrays.fill(password, (byte)0);
+
+ try {
+ if (OpenBSDBCrypt.checkPassword(hashed, array)) {
+ final List roles = cfg.getAsList(credentials.getUsername() + ".roles", Collections.emptyList());
+ final Settings customAttributes = cfg.getAsSettings(credentials.getUsername() + ".attributes");
+
+ if(customAttributes != null) {
+ for(String attributeName: customAttributes.names()) {
+ credentials.addAttribute("attr.internal."+attributeName, customAttributes.get(attributeName));
+ }
+ }
+
+ return new User(credentials.getUsername(), roles, credentials);
+ } else {
+ throw new ElasticsearchSecurityException("password does not match");
+ }
+ } finally {
+ Arrays.fill(wrap.array(), (byte)0);
+ Arrays.fill(buf.array(), '\0');
+ Arrays.fill(array, '\0');
+ }
+ }
+
+ @Override
+ public String getType() {
+ return "internal";
+ }
+
+ private Settings getConfigSettings() {
+ return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_INTERNAL_USERS, false);
+ }
+
+ @Override
+ public void fillRoles(User user, AuthCredentials credentials) throws ElasticsearchSecurityException {
+ final Settings cfg = getConfigSettings();
+ if (cfg == null) {
+ throw new ElasticsearchSecurityException("Internal authentication backend not configured. May be Open Distro Security is not initialized.");
+
+ }
+ final List roles = cfg.getAsList(credentials.getUsername() + ".roles", Collections.emptyList());
+ if(roles != null && !roles.isEmpty() && user != null) {
+ user.addRoles(roles);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java
new file mode 100644
index 0000000000..e1ef693c3b
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthenticationBackend.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal;
+
+import java.nio.file.Path;
+
+import org.elasticsearch.common.settings.Settings;
+
+import com.amazon.opendistroforelasticsearch.security.auth.AuthenticationBackend;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class NoOpAuthenticationBackend implements AuthenticationBackend {
+
+ public NoOpAuthenticationBackend(final Settings settings, final Path configPath) {
+ super();
+ }
+
+ @Override
+ public String getType() {
+ return "noop";
+ }
+
+ @Override
+ public User authenticate(final AuthCredentials credentials) {
+ return new User(credentials.getUsername(), credentials.getBackendRoles(), credentials);
+ }
+
+ @Override
+ public boolean exists(User user) {
+ return true;
+ }
+
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java
new file mode 100644
index 0000000000..0f772cca9c
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/auth/internal/NoOpAuthorizationBackend.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.auth.internal;
+
+import java.nio.file.Path;
+
+import org.elasticsearch.common.settings.Settings;
+
+import com.amazon.opendistroforelasticsearch.security.auth.AuthorizationBackend;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class NoOpAuthorizationBackend implements AuthorizationBackend {
+
+ public NoOpAuthorizationBackend(final Settings settings, final Path configPath) {
+ super();
+ }
+
+ @Override
+ public String getType() {
+ return "noop";
+ }
+
+ @Override
+ public void fillRoles(final User user, final AuthCredentials authCreds) {
+ // no-op
+ }
+
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java
new file mode 100644
index 0000000000..a58ec11957
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceConfig.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.compliance;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.env.Environment;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog;
+import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer;
+import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+
+public class ComplianceConfig {
+
+ private final Logger log = LogManager.getLogger(getClass());
+ private final Settings settings;
+ private final Map> readEnabledFields = new HashMap<>(100);
+ private final List watchedWriteIndices;
+ private DateTimeFormatter auditLogPattern = null;
+ private String auditLogIndex = null;
+ private final boolean logDiffsForWrite;
+ private final boolean logWriteMetadataOnly;
+ private final boolean logReadMetadataOnly;
+ private final boolean logExternalConfig;
+ private final boolean logInternalConfig;
+ private final LoadingCache> cache;
+ private final Set immutableIndicesPatterns;
+ private final byte[] salt16;
+ private final String opendistrosecurityIndex;
+ private final IndexResolverReplacer irr;
+ private final Environment environment;
+ private final AuditLog auditLog;
+ private volatile boolean enabled = true;
+ private volatile boolean externalConfigLogged = false;
+
+ public ComplianceConfig(final Environment environment, final IndexResolverReplacer irr, final AuditLog auditLog) {
+ super();
+ this.settings = environment.settings();
+ this.environment = environment;
+ this.irr = irr;
+ this.auditLog = auditLog;
+ final List watchedReadFields = this.settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS,
+ Collections.emptyList(), false);
+
+ watchedWriteIndices = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, Collections.emptyList());
+ logDiffsForWrite = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, false);
+ logWriteMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, false);
+ logReadMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, false);
+ logExternalConfig = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false);
+ logInternalConfig = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, false);
+ immutableIndicesPatterns = new HashSet(settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList()));
+ final String saltAsString = settings.get(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT, ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT_DEFAULT);
+ final byte[] saltAsBytes = saltAsString.getBytes(StandardCharsets.UTF_8);
+
+ if(saltAsString.equals(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT_DEFAULT)) {
+ log.warn("If you plan to use field masking pls configure "+ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" to be a random string of 16 chars length identical on all nodes");
+ }
+
+ if(saltAsBytes.length < 16) {
+ throw new ElasticsearchException(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" must at least contain 16 bytes");
+ }
+
+ if(saltAsBytes.length > 16) {
+ log.warn(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_SALT+" is greater than 16 bytes. Only the first 16 bytes are used for salting");
+ }
+
+ salt16 = Arrays.copyOf(saltAsBytes, 16);
+ this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
+
+ //opendistro_security.compliance.pii_fields:
+ // - indexpattern,fieldpattern,fieldpattern,....
+ for(String watchedReadField: watchedReadFields) {
+ final List split = new ArrayList<>(Arrays.asList(watchedReadField.split(",")));
+ if(split.isEmpty()) {
+ continue;
+ } else if(split.size() == 1) {
+ readEnabledFields.put(split.get(0), Collections.singleton("*"));
+ } else {
+ Set _fields = new HashSet(split.subList(1, split.size()));
+ readEnabledFields.put(split.get(0), _fields);
+ }
+ }
+
+ final String type = settings.get(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_TYPE_DEFAULT, null);
+ if("internal_elasticsearch".equalsIgnoreCase(type)) {
+ final String index = settings.get(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ES_INDEX,"'security-auditlog-'YYYY.MM.dd");
+ try {
+ auditLogPattern = DateTimeFormat.forPattern(index); //throws IllegalArgumentException if no pattern
+ } catch (IllegalArgumentException e) {
+ //no pattern
+ auditLogIndex = index;
+ } catch (Exception e) {
+ log.error("Unable to check if auditlog index {} is part of compliance setup", index, e);
+ }
+ }
+
+ log.info("PII configuration [auditLogPattern={}, auditLogIndex={}]: {}", auditLogPattern, auditLogIndex, readEnabledFields);
+
+
+ cache = CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .build(new CacheLoader>() {
+ @Override
+ public Set load(String index) throws Exception {
+ return getFieldsForIndex0(index);
+ }
+ });
+ }
+
+ public boolean isLogExternalConfig() {
+ return logExternalConfig;
+ }
+
+ public boolean isExternalConfigLogged() {
+ return externalConfigLogged;
+ }
+
+ public void setExternalConfigLogged(boolean externalConfigLogged) {
+ this.externalConfigLogged = externalConfigLogged;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ //cached
+ @SuppressWarnings("unchecked")
+ private Set getFieldsForIndex0(String index) {
+
+ if(index == null) {
+ return Collections.EMPTY_SET;
+ }
+
+ if(auditLogIndex != null && auditLogIndex.equalsIgnoreCase(index)) {
+ return Collections.EMPTY_SET;
+ }
+
+ if(auditLogPattern != null) {
+ if(index.equalsIgnoreCase(getExpandedIndexName(auditLogPattern, null))) {
+ return Collections.EMPTY_SET;
+ }
+ }
+
+ final Set tmp = new HashSet(100);
+ for(String indexPattern: readEnabledFields.keySet()) {
+ if(indexPattern != null && !indexPattern.isEmpty() && WildcardMatcher.match(indexPattern, index)) {
+ tmp.addAll(readEnabledFields.get(indexPattern));
+ }
+ }
+ return tmp;
+ }
+
+ private String getExpandedIndexName(DateTimeFormatter indexPattern, String index) {
+ if(indexPattern == null) {
+ return index;
+ }
+ return indexPattern.print(DateTime.now(DateTimeZone.UTC));
+ }
+
+ //do not check for isEnabled
+ public boolean writeHistoryEnabledForIndex(String index) {
+
+ if(index == null) {
+ return false;
+ }
+
+ if(opendistrosecurityIndex.equals(index)) {
+ return logInternalConfig;
+ }
+
+ if(auditLogIndex != null && auditLogIndex.equalsIgnoreCase(index)) {
+ return false;
+ }
+
+ if(auditLogPattern != null) {
+ if(index.equalsIgnoreCase(getExpandedIndexName(auditLogPattern, null))) {
+ return false;
+ }
+ }
+
+ return WildcardMatcher.matchAny(watchedWriteIndices, index);
+ }
+
+ //no patterns here as parameters
+ //check for isEnabled
+ public boolean readHistoryEnabledForIndex(String index) {
+
+ if(!this.enabled) {
+ return false;
+ }
+
+ if(opendistrosecurityIndex.equals(index)) {
+ return logInternalConfig;
+ }
+
+ try {
+ return !cache.get(index).isEmpty();
+ } catch (ExecutionException e) {
+ log.error(e);
+ return true;
+ }
+ }
+
+ //no patterns here as parameters
+ //check for isEnabled
+ public boolean readHistoryEnabledForField(String index, String field) {
+
+ if(!this.enabled) {
+ return false;
+ }
+
+ if(opendistrosecurityIndex.equals(index)) {
+ return logInternalConfig;
+ }
+
+ try {
+ final Set fields = cache.get(index);
+ if(fields.isEmpty()) {
+ return false;
+ }
+
+ return WildcardMatcher.matchAny(fields, field);
+ } catch (ExecutionException e) {
+ log.error(e);
+ return true;
+ }
+ }
+
+ public boolean logDiffsForWrite() {
+ return !logWriteMetadataOnly() && logDiffsForWrite;
+ }
+
+ public boolean logWriteMetadataOnly() {
+ return logWriteMetadataOnly;
+ }
+
+ public boolean logReadMetadataOnly() {
+ return logReadMetadataOnly;
+ }
+
+ public Settings getSettings() {
+ return settings;
+ }
+
+ public Environment getEnvironment() {
+ return environment;
+ }
+
+
+ //check for isEnabled
+ public boolean isIndexImmutable(Object request) {
+
+ if(!this.enabled) {
+ return false;
+ }
+
+ if(immutableIndicesPatterns.isEmpty()) {
+ return false;
+ }
+
+ final Resolved resolved = irr.resolveRequest(request);
+ final Set allIndices = resolved.getAllIndices();
+
+ //assert allIndices.size() == 1:"only one index here, not "+allIndices;
+ //assert allIndices.contains("_all"):"no _all in "+allIndices;
+ //assert allIndices.contains("*"):"no * in "+allIndices;
+ //assert allIndices.contains(""):"no EMPTY in "+allIndices;
+
+ return WildcardMatcher.matchAny(immutableIndicesPatterns, allIndices);
+ }
+
+ public byte[] getSalt16() {
+ return salt16.clone();
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java
new file mode 100644
index 0000000000..a383c5f144
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/compliance/ComplianceIndexingOperationListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.compliance;
+
+import org.elasticsearch.index.IndexService;
+import org.elasticsearch.index.shard.IndexingOperationListener;
+
+/**
+ * noop impl
+ *
+ *
+ */
+public class ComplianceIndexingOperationListener implements IndexingOperationListener {
+
+ public void setIs(IndexService is) {
+ //noop
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java
new file mode 100644
index 0000000000..eeadd9860a
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ActionGroupHolder.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.elasticsearch.common.settings.Settings;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+
+public class ActionGroupHolder {
+
+ final ConfigurationRepository configurationRepository;
+
+ public ActionGroupHolder(final ConfigurationRepository configurationRepository) {
+ this.configurationRepository = configurationRepository;
+ }
+
+ public Set getGroupMembers(final String groupname) {
+
+ final Settings actionGroups = getSettings();
+
+ if (actionGroups == null) {
+ return Collections.emptySet();
+ }
+
+ return resolve(actionGroups, groupname);
+ }
+
+ private Set resolve(final Settings actionGroups, final String entry) {
+
+ final Set ret = new HashSet();
+
+ List en = actionGroups.getAsList(entry);
+ if (en.isEmpty()) {
+ // try Open Distro Security format including readonly and permissions key
+ en = actionGroups.getAsList(entry +"." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS);
+ }
+ for (String string: en) {
+ if (actionGroups.names().contains(string)) {
+ ret.addAll(resolve(actionGroups,string));
+ } else {
+ ret.add(string);
+ }
+ }
+ return ret;
+ }
+
+ public Set resolvedActions(final List actions) {
+ final Set resolvedActions = new HashSet();
+ for (String string: actions) {
+ final Set groups = getGroupMembers(string);
+ if (groups.isEmpty()) {
+ resolvedActions.add(string);
+ } else {
+ resolvedActions.addAll(groups);
+ }
+ }
+
+ return resolvedActions;
+ }
+
+ private Settings getSettings() {
+ return configurationRepository.getConfiguration(ConfigConstants.CONFIGNAME_ACTION_GROUPS, false);
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java
new file mode 100644
index 0000000000..6562e384cb
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/AdminDNs.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.settings.Settings;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ListMultimap;
+
+public class AdminDNs {
+
+ protected final Logger log = LogManager.getLogger(AdminDNs.class);
+ private final Set adminDn = new HashSet();
+ private final Set adminUsernames = new HashSet();
+ private final ListMultimap allowedImpersonations = ArrayListMultimap. create();
+ private final ListMultimap allowedRestImpersonations = ArrayListMultimap. create();
+ private boolean injectUserEnabled;
+ private boolean injectAdminUserEnabled;
+
+ public AdminDNs(final Settings settings) {
+
+ this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false);
+ this.injectAdminUserEnabled = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false);
+
+ final List adminDnsA = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList());
+
+ for (String dn:adminDnsA) {
+ try {
+ log.debug("{} is registered as an admin dn", dn);
+ adminDn.add(new LdapName(dn));
+ } catch (final InvalidNameException e) {
+ // make sure to log correctly depending on user injection settings
+ if (injectUserEnabled && injectAdminUserEnabled) {
+ if (log.isDebugEnabled()) {
+ log.debug("Admin DN not an LDAP name, but admin user injection enabled. Will add {} to admin usernames", dn);
+ }
+ adminUsernames.add(dn);
+ } else {
+ log.error("Unable to parse admin dn {}",dn, e);
+ }
+ }
+ }
+
+ log.debug("Loaded {} admin DN's {}",adminDn.size(), adminDn);
+
+ final Settings impersonationDns = settings.getByPrefix(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+".");
+
+ for (String dnString:impersonationDns.keySet()) {
+ try {
+ allowedImpersonations.putAll(new LdapName(dnString), settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_IMPERSONATION_DN+"."+dnString));
+ } catch (final InvalidNameException e) {
+ log.error("Unable to parse allowedImpersonations dn {}",dnString, e);
+ }
+ }
+
+ log.debug("Loaded {} impersonation DN's {}",allowedImpersonations.size(), allowedImpersonations);
+
+ final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".");
+
+ for (String user:impersonationUsersRest.keySet()) {
+ allowedRestImpersonations.putAll(user, settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+"."+user));
+ }
+
+ log.debug("Loaded {} impersonation users for REST {}",allowedRestImpersonations.size(), allowedRestImpersonations);
+ }
+
+ public boolean isAdmin(User user) {
+ if (isAdminDN(user.getName())) {
+ return true;
+ }
+
+ // ThreadContext injected user, may be admin user, only if both flags are enabled and user is injected
+ if (injectUserEnabled && injectAdminUserEnabled && user.isInjected() && adminUsernames.contains(user.getName())) {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean isAdminDN(String dn) {
+
+ if(dn == null) return false;
+
+ try {
+ return isAdminDN(new LdapName(dn));
+ } catch (InvalidNameException e) {
+ return false;
+ }
+ }
+
+ private boolean isAdminDN(LdapName dn) {
+ if(dn == null) return false;
+
+ boolean isAdmin = adminDn.contains(dn);
+
+ if (log.isTraceEnabled()) {
+ log.trace("Is principal {} an admin cert? {}", dn.toString(), isAdmin);
+ }
+
+ return isAdmin;
+ }
+
+ public boolean isTransportImpersonationAllowed(LdapName dn, String impersonated) {
+ if(dn == null) return false;
+
+ if(isAdminDN(dn)) {
+ return true;
+ }
+
+ return WildcardMatcher.matchAny(this.allowedImpersonations.get(dn), impersonated);
+ }
+
+ public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) {
+ if(originalUser == null) {
+ return false;
+ }
+ return WildcardMatcher.matchAny(this.allowedRestImpersonations.get(originalUser), impersonated);
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java
new file mode 100644
index 0000000000..5b0a145c24
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ClusterInfoHolder.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.Version;
+import org.elasticsearch.cluster.ClusterChangedEvent;
+import org.elasticsearch.cluster.ClusterState;
+import org.elasticsearch.cluster.ClusterStateListener;
+import org.elasticsearch.cluster.metadata.IndexMetaData;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.index.Index;
+
+public class ClusterInfoHolder implements ClusterStateListener {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private volatile Boolean has5xNodes = null;
+ private volatile Boolean has5xIndices = null;
+ private volatile DiscoveryNodes nodes = null;
+ private volatile Boolean isLocalNodeElectedMaster = null;;
+
+ @Override
+ public void clusterChanged(ClusterChangedEvent event) {
+ if(has5xNodes == null || event.nodesChanged()) {
+ has5xNodes = Boolean.valueOf(clusterHas5xNodes(event.state()));
+ if(log.isTraceEnabled()) {
+ log.trace("has5xNodes: {}", has5xNodes);
+ }
+ }
+
+ final List indicesCreated = event.indicesCreated();
+ final List indicesDeleted = event.indicesDeleted();
+ if(has5xIndices == null || !indicesCreated.isEmpty() || !indicesDeleted.isEmpty()) {
+ has5xIndices = Boolean.valueOf(clusterHas5xIndices(event.state()));
+ if(log.isTraceEnabled()) {
+ log.trace("has5xIndices: {}", has5xIndices);
+ }
+ }
+
+ if(nodes == null || event.nodesChanged()) {
+ nodes = event.state().nodes();
+ if(log.isDebugEnabled()) {
+ log.debug("Cluster Info Holder now initialized for 'nodes'");
+ }
+ }
+
+ isLocalNodeElectedMaster = event.localNodeMaster()?Boolean.TRUE:Boolean.FALSE;
+ }
+
+ public Boolean getHas5xNodes() {
+ return has5xNodes;
+ }
+
+ public Boolean getHas5xIndices() {
+ return has5xIndices;
+ }
+
+ public Boolean isLocalNodeElectedMaster() {
+ return isLocalNodeElectedMaster;
+ }
+
+ public Boolean hasNode(DiscoveryNode node) {
+ if(nodes == null) {
+ if(log.isDebugEnabled()) {
+ log.debug("Cluster Info Holder not initialized yet for 'nodes'");
+ }
+ return null;
+ }
+
+ return nodes.nodeExists(node)?Boolean.TRUE:Boolean.FALSE;
+ }
+
+ private static boolean clusterHas5xNodes(ClusterState state) {
+ return state.nodes().getMinNodeVersion().before(Version.V_6_0_0_alpha1);
+ }
+
+ private static boolean clusterHas5xIndices(ClusterState state) {
+ final Iterator indices = state.metaData().indices().valuesIt();
+ for(;indices.hasNext();) {
+ final IndexMetaData indexMetaData = indices.next();
+ if(indexMetaData.getCreationVersion().before(Version.V_6_0_0_alpha1)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java
new file mode 100644
index 0000000000..80a38c78f8
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/CompatConfig.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.env.Environment;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+
+
+public class CompatConfig implements ConfigurationChangeListener {
+
+ private final Logger log = LogManager.getLogger(getClass());
+ private final Settings staticSettings;
+ private Settings dynamicSecurityConfig;
+
+ public CompatConfig(final Environment environment) {
+ super();
+ this.staticSettings = environment.settings();
+ }
+
+ @Override
+ public void onChange(final Settings dynamicSecurityConfig) {
+ this.dynamicSecurityConfig = dynamicSecurityConfig;
+ log.debug("dynamicSecurityConfig updated?: {}", (dynamicSecurityConfig != null));
+ }
+
+ //true is default
+ public boolean restAuthEnabled() {
+ final boolean restInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, false);
+
+ if(restInitiallyDisabled) {
+ if(dynamicSecurityConfig == null) {
+ if(log.isTraceEnabled()) {
+ log.trace("dynamicSecurityConfig is null, initially static restDisabled");
+ }
+ return false;
+ } else {
+ final boolean restDynamicallyDisabled = dynamicSecurityConfig.getAsBoolean("opendistro_security.dynamic.disable_rest_auth", false);
+ if(log.isTraceEnabled()) {
+ log.trace("opendistro_security.dynamic.disable_rest_auth {}", restDynamicallyDisabled);
+ }
+ return !restDynamicallyDisabled;
+ }
+ } else {
+ return true;
+ }
+
+ }
+
+ //true is default
+ public boolean transportInterClusterAuthEnabled() {
+ final boolean interClusterAuthInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, false);
+
+ if(interClusterAuthInitiallyDisabled) {
+ if(dynamicSecurityConfig == null) {
+ if(log.isTraceEnabled()) {
+ log.trace("dynamicSecurityConfig is null, initially static interClusterAuthDisabled");
+ }
+ return false;
+ } else {
+ final boolean interClusterAuthDynamicallyDisabled = dynamicSecurityConfig.getAsBoolean("opendistro_security.dynamic.disable_intertransport_auth", false);
+ if(log.isTraceEnabled()) {
+ log.trace("opendistro_security.dynamic.disable_intertransport_auth {}", interClusterAuthDynamicallyDisabled);
+ }
+ return !interClusterAuthDynamicallyDisabled;
+ }
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java
new file mode 100644
index 0000000000..cd947c36a8
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import org.elasticsearch.action.get.MultiGetResponse.Failure;
+import org.elasticsearch.common.settings.Settings;
+
+public interface ConfigCallback {
+
+ void success(String id, Settings settings);
+ void noData(String id);
+ void singleFailure(Failure failure);
+ void failure(Throwable t);
+
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java
new file mode 100644
index 0000000000..7be7fb6dd6
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationChangeListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+
+import org.elasticsearch.common.settings.Settings;
+
+/**
+ * Callback function on change particular configuration
+ */
+public interface ConfigurationChangeListener {
+
+ /**
+ * @param configuration not null updated configuration on that was subscribe current listener
+ */
+ void onChange(Settings configuration);
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java
new file mode 100644
index 0000000000..e359121682
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationLoader.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.get.GetResponse;
+import org.elasticsearch.action.get.MultiGetItemResponse;
+import org.elasticsearch.action.get.MultiGetRequest;
+import org.elasticsearch.action.get.MultiGetResponse;
+import org.elasticsearch.action.get.MultiGetResponse.Failure;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler;
+
+class ConfigurationLoader {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private final Client client;
+ //private final ThreadContext threadContext;
+ private final String opendistrosecurityIndex;
+
+ ConfigurationLoader(final Client client, ThreadPool threadPool, final Settings settings) {
+ super();
+ this.client = client;
+ //this.threadContext = threadPool.getThreadContext();
+ this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
+ log.debug("Index is: {}", opendistrosecurityIndex);
+ }
+
+ Map load(final String[] events, long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
+ final CountDownLatch latch = new CountDownLatch(events.length);
+ final Map rs = new HashMap(events.length);
+
+ loadAsync(events, new ConfigCallback() {
+
+ @Override
+ public void success(String id, Settings settings) {
+ if(latch.getCount() <= 0) {
+ log.error("Latch already counted down (for {} of {}) (index={})", id, Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ rs.put(id, settings);
+ latch.countDown();
+ if(log.isDebugEnabled()) {
+ log.debug("Received config for {} (of {}) with current latch value={}", id, Arrays.toString(events), latch.getCount());
+ }
+ }
+
+ @Override
+ public void singleFailure(Failure failure) {
+ log.error("Failure {} retrieving configuration for {} (index={})", failure==null?null:failure.getMessage(), Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ @Override
+ public void noData(String id) {
+ log.warn("No data for {} while retrieving configuration for {} (index={})", id, Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ @Override
+ public void failure(Throwable t) {
+ log.error("Exception {} while retrieving configuration for {} (index={})",t,t.toString(), Arrays.toString(events), opendistrosecurityIndex);
+ }
+ });
+
+ if(!latch.await(timeout, timeUnit)) {
+ //timeout
+ throw new TimeoutException("Timeout after "+timeout+""+timeUnit+" while retrieving configuration for "+Arrays.toString(events)+ "(index="+opendistrosecurityIndex+")");
+ }
+
+ return rs;
+ }
+
+ void loadAsync(final String[] events, final ConfigCallback callback) {
+ if(events == null || events.length == 0) {
+ log.warn("No config events requested to load");
+ return;
+ }
+
+ final MultiGetRequest mget = new MultiGetRequest();
+
+ for (int i = 0; i < events.length; i++) {
+ final String event = events[i];
+ mget.add(opendistrosecurityIndex, "security", event);
+ }
+
+ mget.refresh(true);
+ mget.realtime(true);
+
+ //try(StoredContext ctx = threadContext.stashContext()) {
+ // threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true");
+ {
+ client.multiGet(mget, new ActionListener() {
+ @Override
+ public void onResponse(MultiGetResponse response) {
+ MultiGetItemResponse[] responses = response.getResponses();
+ for (int i = 0; i < responses.length; i++) {
+ MultiGetItemResponse singleResponse = responses[i];
+ if(singleResponse != null && !singleResponse.isFailed()) {
+ GetResponse singleGetResponse = singleResponse.getResponse();
+ if(singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) {
+ //success
+ final Settings _settings = toSettings(singleGetResponse.getSourceAsBytesRef(), singleGetResponse.getId());
+ if(_settings != null) {
+ callback.success(singleGetResponse.getId(), _settings);
+ } else {
+ log.error("Cannot parse settings for "+singleGetResponse.getId());
+ }
+ } else {
+ //does not exist or empty source
+ callback.noData(singleGetResponse.getId());
+ }
+ } else {
+ //failure
+ callback.singleFailure(singleResponse==null?null:singleResponse.getFailure());
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ callback.failure(e);
+ }
+ });
+ }
+ }
+
+ private Settings toSettings(final BytesReference ref, final String id) {
+ if (ref == null || ref.length() == 0) {
+ log.error("Empty or null byte reference for {}", id);
+ return null;
+ }
+
+ XContentParser parser = null;
+
+ try {
+ parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, ref, XContentType.JSON);
+ parser.nextToken();
+ parser.nextToken();
+
+ if(!id.equals((parser.currentName()))) {
+ log.error("Cannot parse config for type {} because {}!={}", id, id, parser.currentName());
+ return null;
+ }
+
+ parser.nextToken();
+
+ return Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(parser.binaryValue()), true).build();
+ } catch (final IOException e) {
+ throw ExceptionsHelper.convertToElastic(e);
+ } finally {
+ if(parser != null) {
+ try {
+ parser.close();
+ } catch (IOException e) {
+ //ignore
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java
new file mode 100644
index 0000000000..cbee5fb77c
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/ConfigurationRepository.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.elasticsearch.common.settings.Settings;
+
+/**
+ * Abstraction layer over Open Distro Security configuration repository
+ */
+public interface ConfigurationRepository {
+
+ /**
+ * Load configuration from persistence layer
+ *
+ * @param configurationType not null configuration identifier
+ * @return configuration found by specified type in persistence layer or {@code null} if persistence layer
+ * doesn't have configuration by requested type, or persistence layer not ready yet
+ * @throws NullPointerException if specified configuration type is null or empty
+ */
+
+ Settings getConfiguration(String configurationType, boolean triggerComplianceWhenCached);
+
+ /**
+ * Bulk load configuration from persistence layer
+ *
+ * @param configTypes not null collection with not null configuration identifiers by that need load configurations
+ * @return not null map where key it configuration type for found configuration and value it not null {@link Settings}
+ * that represent configuration for correspond type. If by requested type configuration absent in persistence layer,
+ * they will be absent in result map
+ * @throws NullPointerException if specified collection with type null or contain null or empty types
+ */
+ //Map getConfiguration(Collection configTypes);
+
+ /**
+ * Bulk reload configuration from persistence layer. If configuration was modify manually bypassing business logic define
+ * in {@link ConfigurationRepository}, this method should catch up it logic. This method can be very slow, because it skip
+ * all caching logic and should be use only as a last resort.
+ *
+ * @param configTypes not null collection with not null configuration identifiers by that need load configurations
+ * @return not null map where key it configuration type for found configuration and value it not null {@link Settings}
+ * that represent configuration for correspond type. If by requested type configuration absent in persistence layer,
+ * they will be absent in result map
+ * @throws NullPointerException if specified collection with type null or contain null or empty types
+ */
+ Map reloadConfiguration(Collection configTypes);
+
+ /**
+ * Save changed configuration in persistence layer. After save, changes will be available for
+ * read via {@link ConfigurationRepository#getConfiguration(String)}
+ *
+ * @param configurationType not null configuration identifier
+ * @param settings not null configuration that need persist
+ * @throws NullPointerException if specified configuration is null or configuration type is null or empty
+ */
+ void persistConfiguration(String configurationType, Settings settings);
+
+ /**
+ * Subscribe on configuration change
+ *
+ * @param configurationType not null and not empty configuration type of which changes need notify listener
+ * @param listener not null callback function that will be execute when specified type will modify
+ * @throws NullPointerException if specified configuration type is null or empty, or callback function is null
+ */
+ void subscribeOnChange(String configurationType, ConfigurationChangeListener listener);
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java
new file mode 100644
index 0000000000..d479f3f796
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/DlsFlsRequestValve.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+
+public interface DlsFlsRequestValve {
+
+ /**
+ *
+ * @param request
+ * @param listener
+ * @return false to stop
+ */
+ boolean invoke(ActionRequest request, ActionListener> listener, Map> allowedFlsFields, final Map> maskedFields, Map> queries);
+
+ public static class NoopDlsFlsRequestValve implements DlsFlsRequestValve {
+
+ @Override
+ public boolean invoke(ActionRequest request, ActionListener> listener, Map> allowedFlsFields, final Map> maskedFields, Map> queries) {
+ return true;
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java
new file mode 100644
index 0000000000..545b4caa00
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/EmptyFilterLeafReader.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.FilterDirectoryReader;
+import org.apache.lucene.index.FilterLeafReader;
+import org.apache.lucene.index.LeafMetaData;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.index.PointValues;
+import org.apache.lucene.index.SortedDocValues;
+import org.apache.lucene.index.SortedNumericDocValues;
+import org.apache.lucene.index.SortedSetDocValues;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.util.Bits;
+import org.elasticsearch.index.mapper.MapperService;
+
+import com.google.common.collect.Sets;
+
+class EmptyFilterLeafReader extends FilterLeafReader {
+
+ private static final Set metaFields = Sets.union(Sets.newHashSet("_version"),
+ Sets.newHashSet(MapperService.getAllMetaFields()));
+
+ private final FieldInfo[] fi;
+
+ EmptyFilterLeafReader(final LeafReader delegate) {
+ super(delegate);
+ final FieldInfos infos = delegate.getFieldInfos();
+ final List lfi = new ArrayList(metaFields.size());
+ for(String metaField: metaFields) {
+ final FieldInfo _fi = infos.fieldInfo(metaField);
+ if(_fi != null) {
+ lfi.add(_fi);
+ }
+ }
+ fi = lfi.toArray(new FieldInfo[0]);
+ }
+
+ private static class EmptySubReaderWrapper extends FilterDirectoryReader.SubReaderWrapper {
+
+ @Override
+ public LeafReader wrap(final LeafReader reader) {
+ return new EmptyFilterLeafReader(reader);
+ }
+
+ }
+
+ static class EmptyDirectoryReader extends FilterDirectoryReader {
+
+ public EmptyDirectoryReader(final DirectoryReader in) throws IOException {
+ super(in, new EmptySubReaderWrapper());
+ }
+
+ @Override
+ protected DirectoryReader doWrapDirectoryReader(final DirectoryReader in) throws IOException {
+ return new EmptyDirectoryReader(in);
+ }
+
+ @Override
+ public CacheHelper getReaderCacheHelper() {
+ return in.getReaderCacheHelper();
+ }
+ }
+
+ private boolean isMeta(String field) {
+ return metaFields.contains(field);
+ }
+
+ @Override
+ public FieldInfos getFieldInfos() {
+ return new FieldInfos(fi);
+ }
+
+ @Override
+ public NumericDocValues getNumericDocValues(final String field) throws IOException {
+ return isMeta(field) ? in.getNumericDocValues(field) : null;
+ }
+
+ @Override
+ public BinaryDocValues getBinaryDocValues(final String field) throws IOException {
+ return isMeta(field) ? in.getBinaryDocValues(field) : null;
+ }
+
+ @Override
+ public SortedDocValues getSortedDocValues(final String field) throws IOException {
+ return isMeta(field) ? in.getSortedDocValues(field) : null;
+ }
+
+ @Override
+ public SortedNumericDocValues getSortedNumericDocValues(final String field) throws IOException {
+ return isMeta(field) ? in.getSortedNumericDocValues(field) : null;
+ }
+
+ @Override
+ public SortedSetDocValues getSortedSetDocValues(final String field) throws IOException {
+ return isMeta(field) ? in.getSortedSetDocValues(field) : null;
+ }
+
+ @Override
+ public NumericDocValues getNormValues(final String field) throws IOException {
+ return isMeta(field) ? in.getNormValues(field) : null;
+ }
+
+ @Override
+ public PointValues getPointValues(String field) throws IOException {
+ return isMeta(field) ? in.getPointValues(field) : null;
+ }
+
+ @Override
+ public Terms terms(String field) throws IOException {
+ return isMeta(field) ? in.terms(field) : null;
+ }
+
+ @Override
+ public LeafMetaData getMetaData() {
+ return in.getMetaData();
+ }
+
+ @Override
+ public Bits getLiveDocs() {
+ return new Bits.MatchNoBits(0);
+ }
+
+ @Override
+ public int numDocs() {
+ return 0;
+ }
+
+ @Override
+ public LeafReader getDelegate() {
+ return in;
+ }
+
+ @Override
+ public int maxDoc() {
+ return in.maxDoc();
+ }
+
+ @Override
+ public CacheHelper getCoreCacheHelper() {
+ return in.getCoreCacheHelper();
+ }
+
+ @Override
+ public CacheHelper getReaderCacheHelper() {
+ return in.getReaderCacheHelper();
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java
new file mode 100644
index 0000000000..7b52b880f6
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/IndexBaseConfigurationRepository.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
+import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
+import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsRequest;
+import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.component.LifecycleListener;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
+import org.elasticsearch.env.Environment;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog;
+import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig;
+import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigHelper;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+public class IndexBaseConfigurationRepository implements ConfigurationRepository {
+ private static final Logger LOGGER = LogManager.getLogger(IndexBaseConfigurationRepository.class);
+ private static final Pattern DLS_PATTERN = Pattern.compile(".+\\.indices\\..+\\._dls_=.+", Pattern.DOTALL);
+ private static final Pattern FLS_PATTERN = Pattern.compile(".+\\.indices\\..+\\._fls_\\.[0-9]+=.+", Pattern.DOTALL);
+
+ private final String opendistrosecurityIndex;
+ private final Client client;
+ private final ConcurrentMap typeToConfig;
+ private final Multimap configTypeToChancheListener;
+ private final ConfigurationLoader cl;
+ private final LegacyConfigurationLoader legacycl;
+ private final Settings settings;
+ private final ClusterService clusterService;
+ private final AuditLog auditLog;
+ private final ComplianceConfig complianceConfig;
+ private ThreadPool threadPool;
+
+ private IndexBaseConfigurationRepository(Settings settings, final Path configPath, ThreadPool threadPool,
+ Client client, ClusterService clusterService, AuditLog auditLog, ComplianceConfig complianceConfig) {
+ this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
+ this.settings = settings;
+ this.client = client;
+ this.threadPool = threadPool;
+ this.clusterService = clusterService;
+ this.auditLog = auditLog;
+ this.complianceConfig = complianceConfig;
+ this.typeToConfig = Maps.newConcurrentMap();
+ this.configTypeToChancheListener = ArrayListMultimap.create();
+ cl = new ConfigurationLoader(client, threadPool, settings);
+ legacycl = new LegacyConfigurationLoader(client, threadPool, settings);
+
+ final AtomicBoolean installDefaultConfig = new AtomicBoolean();
+
+ clusterService.addLifecycleListener(new LifecycleListener() {
+
+ @Override
+ public void afterStart() {
+
+ final Thread bgThread = new Thread(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+
+ if(installDefaultConfig.get()) {
+
+ try {
+ String lookupDir = System.getProperty("security.default_init.dir");
+ final String cd = lookupDir != null? (lookupDir+"/") : new Environment(settings, configPath).pluginsFile().toAbsolutePath().toString()+"/opendistro_security/securityconfig/";
+ File confFile = new File(cd+"config.yml");
+ if(confFile.exists()) {
+ final ThreadContext threadContext = threadPool.getThreadContext();
+ try(StoredContext ctx = threadContext.stashContext()) {
+ threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true");
+ LOGGER.info("Will create {} index so we can apply default config", opendistrosecurityIndex);
+
+ Map indexSettings = new HashMap<>();
+ indexSettings.put("index.number_of_shards", 1);
+ indexSettings.put("index.auto_expand_replicas", "0-all");
+
+ boolean ok = client.admin().indices().create(new CreateIndexRequest(opendistrosecurityIndex)
+ .settings(indexSettings))
+ .actionGet().isAcknowledged();
+ if(ok) {
+ ConfigHelper.uploadFile(client, cd+"config.yml", opendistrosecurityIndex, "config");
+ ConfigHelper.uploadFile(client, cd+"roles.yml", opendistrosecurityIndex, "roles");
+ ConfigHelper.uploadFile(client, cd+"roles_mapping.yml", opendistrosecurityIndex, "rolesmapping");
+ ConfigHelper.uploadFile(client, cd+"internal_users.yml", opendistrosecurityIndex, "internalusers");
+ ConfigHelper.uploadFile(client, cd+"action_groups.yml", opendistrosecurityIndex, "actiongroups");
+ LOGGER.info("Default config applied");
+ }
+ }
+ } else {
+ LOGGER.error("{} does not exist", confFile.getAbsolutePath());
+ }
+ } catch (Exception e) {
+ LOGGER.debug("Cannot apply default config (this is not an error!) due to {}", e.getMessage());
+ }
+ }
+
+ LOGGER.debug("Node started, try to initialize it. Wait for at least yellow cluster state....");
+ ClusterHealthResponse response = null;
+ try {
+ response = client.admin().cluster().health(new ClusterHealthRequest(opendistrosecurityIndex).waitForYellowStatus()).actionGet();
+ } catch (Exception e1) {
+ LOGGER.debug("Catched a {} but we just try again ...", e1.toString());
+ }
+
+ while(response == null || response.isTimedOut() || response.getStatus() == ClusterHealthStatus.RED) {
+ LOGGER.debug("index '{}' not healthy yet, we try again ... (Reason: {})", opendistrosecurityIndex, response==null?"no response":(response.isTimedOut()?"timeout":"other, maybe red cluster"));
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e1) {
+ //ignore
+ Thread.currentThread().interrupt();
+ }
+ try {
+ response = client.admin().cluster().health(new ClusterHealthRequest(opendistrosecurityIndex).waitForYellowStatus()).actionGet();
+ } catch (Exception e1) {
+ LOGGER.debug("Catched again a {} but we just try again ...", e1.toString());
+ }
+ continue;
+ }
+
+ while(true) {
+ try {
+ LOGGER.debug("Try to load config ...");
+ reloadConfiguration(Arrays.asList(new String[] { "config", "roles", "rolesmapping", "internalusers", "actiongroups"} ));
+ break;
+ } catch (Exception e) {
+ LOGGER.debug("Unable to load configuration due to {}", String.valueOf(ExceptionUtils.getRootCause(e)));
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e1) {
+ Thread.currentThread().interrupt();
+ LOGGER.debug("Thread was interrupted so we cancel initialization");
+ break;
+ }
+ }
+ }
+
+ LOGGER.info("Node '{}' initialized", clusterService.localNode().getName());
+
+ } catch (Exception e) {
+ LOGGER.error("Unexpected exception while initializing node "+e, e);
+ }
+ }
+ });
+
+ LOGGER.info("Check if "+opendistrosecurityIndex+" index exists ...");
+
+ try {
+
+ IndicesExistsRequest ier = new IndicesExistsRequest(opendistrosecurityIndex)
+ .masterNodeTimeout(TimeValue.timeValueMinutes(1));
+
+ final ThreadContext threadContext = threadPool.getThreadContext();
+
+ try(StoredContext ctx = threadContext.stashContext()) {
+ threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true");
+
+ client.admin().indices().exists(ier, new ActionListener() {
+
+ @Override
+ public void onResponse(IndicesExistsResponse response) {
+ if(response != null && response.isExists()) {
+ bgThread.start();
+ } else {
+ if(settings.get("tribe.name", null) == null && settings.getByPrefix("tribe").size() > 0) {
+ LOGGER.info("{} index does not exist yet, but we are a tribe node. So we will load the config anyhow until we got it ...", opendistrosecurityIndex);
+ bgThread.start();
+ } else {
+
+ if(settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false)){
+ LOGGER.info("{} index does not exist yet, so we create a default config", opendistrosecurityIndex);
+ installDefaultConfig.set(true);
+ bgThread.start();
+ } else {
+ LOGGER.info("{} index does not exist yet, so no need to load config on node startup. Use securityadmin to initialize cluster", opendistrosecurityIndex);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ LOGGER.error("Failure while checking {} index {}",e, opendistrosecurityIndex, e);
+ bgThread.start();
+ }
+ });
+ }
+ } catch (Throwable e2) {
+ LOGGER.error("Failure while executing IndicesExistsRequest {}",e2, e2);
+ bgThread.start();
+ }
+ }
+ });
+ }
+
+
+ public static ConfigurationRepository create(Settings settings, final Path configPath, final ThreadPool threadPool, Client client, ClusterService clusterService, AuditLog auditLog, ComplianceConfig complianceConfig) {
+ final IndexBaseConfigurationRepository repository = new IndexBaseConfigurationRepository(settings, configPath, threadPool, client, clusterService, auditLog, complianceConfig);
+ return repository;
+ }
+
+ @Override
+ public Settings getConfiguration(String configurationType, boolean triggerComplianceWhenCached) {
+
+ Settings result = typeToConfig.get(configurationType);
+
+ if (result != null) {
+
+ if(triggerComplianceWhenCached && complianceConfig.isEnabled()) {
+ Map fields = new HashMap();
+ fields.put(configurationType, Strings.toString(result));
+ auditLog.logDocumentRead(this.opendistrosecurityIndex, configurationType, null, fields, complianceConfig);
+ }
+ return result;
+ }
+
+ Map loaded = loadConfigurations(Collections.singleton(configurationType));
+
+ result = loaded.get(configurationType);
+
+ return putSettingsToCache(configurationType, result);
+ }
+
+ private Settings putSettingsToCache(String configurationType, Settings result) {
+ if (result != null) {
+ typeToConfig.putIfAbsent(configurationType, result);
+ }
+
+ return typeToConfig.get(configurationType);
+ }
+
+
+ /*@Override
+ public Map getConfiguration(Collection configTypes) {
+ List typesToLoad = Lists.newArrayList();
+ Map result = Maps.newHashMap();
+
+ for (String type : configTypes) {
+ Settings conf = typeToConfig.get(type);
+ if (conf != null) {
+ result.put(type, conf);
+ } else {
+ typesToLoad.add(type);
+ }
+ }
+
+ if (typesToLoad.isEmpty()) {
+ return result;
+ }
+
+ Map loaded = loadConfigurations(typesToLoad);
+
+ for (Map.Entry entry : loaded.entrySet()) {
+ Settings conf = putSettingsToCache(entry.getKey(), entry.getValue());
+
+ if (conf != null) {
+ result.put(entry.getKey(), conf);
+ }
+ }
+
+ return result;
+ }*/
+
+
+ @Override
+ public Map reloadConfiguration(Collection configTypes) {
+ Map loaded = loadConfigurations(configTypes);
+ typeToConfig.clear();
+ typeToConfig.putAll(loaded);
+ notifyAboutChanges(loaded);
+
+ return loaded;
+ }
+
+ @Override
+ public void persistConfiguration(String configurationType, Settings settings) {
+ //TODO should be use from com.amazon.opendistroforelasticsearch.security.tools.OpenDistroSecurityAdmin
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public synchronized void subscribeOnChange(String configurationType, ConfigurationChangeListener listener) {
+ LOGGER.debug("Subscribe on configuration changes by type {} with listener {}", configurationType, listener);
+ configTypeToChancheListener.put(configurationType, listener);
+ }
+
+ private synchronized void notifyAboutChanges(Map typeToConfig) {
+ for (Map.Entry entry : configTypeToChancheListener.entries()) {
+ String type = entry.getKey();
+ ConfigurationChangeListener listener = entry.getValue();
+
+ Settings settings = typeToConfig.get(type);
+
+ if (settings == null) {
+ continue;
+ }
+
+ LOGGER.debug("Notify {} listener about change configuration with type {}", listener, type);
+ listener.onChange(settings);
+ }
+ }
+
+
+ private Map loadConfigurations(Collection configTypes) {
+
+ final ThreadContext threadContext = threadPool.getThreadContext();
+ final Map retVal = new HashMap();
+ //final List exception = new ArrayList(1);
+ // final CountDownLatch latch = new CountDownLatch(1);
+
+ try(StoredContext ctx = threadContext.stashContext()) {
+ threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true");
+
+ boolean securityIndexExists = clusterService.state().metaData().hasConcreteIndex(this.opendistrosecurityIndex);
+
+ if(securityIndexExists) {
+ if(clusterService.state().metaData().index(this.opendistrosecurityIndex).mapping("config") != null) {
+ //legacy layout
+ LOGGER.debug("security index exists and was created before ES 6 (legacy layout)");
+ retVal.putAll(validate(legacycl.loadLegacy(configTypes.toArray(new String[0]), 5, TimeUnit.SECONDS), configTypes.size()));
+ } else {
+ LOGGER.debug("security index exists and was created with ES 6 (new layout)");
+ retVal.putAll(validate(cl.load(configTypes.toArray(new String[0]), 5, TimeUnit.SECONDS), configTypes.size()));
+ }
+ } else {
+ //wait (and use new layout)
+ LOGGER.debug("security index not exists (yet)");
+ retVal.putAll(validate(cl.load(configTypes.toArray(new String[0]), 30, TimeUnit.SECONDS), configTypes.size()));
+ }
+
+ } catch (Exception e) {
+ throw new ElasticsearchException(e);
+ }
+ return retVal;
+ }
+
+ private Map validate(Map conf, int expectedSize) throws InvalidConfigException {
+
+ if(conf == null || conf.size() != expectedSize) {
+ throw new InvalidConfigException("Retrieved only partial configuration");
+ }
+
+ final Settings roles = conf.get("roles");
+ final String rolesDelimited;
+
+ if (roles != null && (rolesDelimited = roles.toDelimitedString('#')) != null) {
+
+ //.indices.._dls_= OK
+ //.indices.._fls_.= OK
+
+ final String[] rolesString = rolesDelimited.split("#");
+
+ for (String role : rolesString) {
+ if (role.contains("_fls_.") && !FLS_PATTERN.matcher(role).matches()) {
+ LOGGER.error("Invalid FLS configuration detected, FLS/DLS will not work correctly: {}", role);
+ }
+
+ if (role.contains("_dls_=") && !DLS_PATTERN.matcher(role).matches()) {
+ LOGGER.error("Invalid DLS configuration detected, FLS/DLS will not work correctly: {}", role);
+ }
+ }
+ }
+
+ return conf;
+ }
+
+ private static String formatDate(long date) {
+ return new SimpleDateFormat("yyyy-MM-dd").format(new Date(date));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java
new file mode 100644
index 0000000000..5b6d73e553
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/InvalidConfigException.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+public class InvalidConfigException extends Exception {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ public InvalidConfigException() {
+ super();
+ }
+
+ public InvalidConfigException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+
+ public InvalidConfigException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public InvalidConfigException(String message) {
+ super(message);
+ }
+
+ public InvalidConfigException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java
new file mode 100644
index 0000000000..18ae4c74b0
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/LegacyConfigurationLoader.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ExceptionsHelper;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.get.GetResponse;
+import org.elasticsearch.action.get.MultiGetItemResponse;
+import org.elasticsearch.action.get.MultiGetRequest;
+import org.elasticsearch.action.get.MultiGetResponse;
+import org.elasticsearch.action.get.MultiGetResponse.Failure;
+import org.elasticsearch.client.Client;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.OpenDistroSecurityDeprecationHandler;
+
+class LegacyConfigurationLoader {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private final Client client;
+ //private final ThreadContext threadContext;
+ private final String opendistrosecurityIndex;
+
+ LegacyConfigurationLoader(final Client client, ThreadPool threadPool, final Settings settings) {
+ super();
+ this.client = client;
+ //this.threadContext = threadPool.getThreadContext();
+ this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
+ log.debug("Index is: {}", opendistrosecurityIndex);
+ }
+
+ Map loadLegacy(final String[] events, long timeout, TimeUnit timeUnit) throws InterruptedException, TimeoutException {
+ final CountDownLatch latch = new CountDownLatch(events.length);
+ final Map rs = new HashMap(events.length);
+
+ loadAsyncLegacy(events, new ConfigCallback() {
+
+ @Override
+ public void success(String type, Settings settings) {
+ if(latch.getCount() <= 0) {
+ log.error("Latch already counted down (for {} of {}) (index={})", type, Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ rs.put(type, settings);
+ latch.countDown();
+ if(log.isDebugEnabled()) {
+ log.debug("Received config for {} (of {}) with current latch value={}", type, Arrays.toString(events), latch.getCount());
+ }
+ }
+
+ @Override
+ public void singleFailure(Failure failure) {
+ log.error("Failure {} retrieving configuration for {} (index={})", failure==null?null:failure.getMessage(), Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ @Override
+ public void noData(String type) {
+ log.warn("No data for {} while retrieving configuration for {} (index={})", type, Arrays.toString(events), opendistrosecurityIndex);
+ }
+
+ @Override
+ public void failure(Throwable t) {
+ log.error("Exception {} while retrieving configuration for {} (index={})",t,t.toString(), Arrays.toString(events), opendistrosecurityIndex);
+ }
+ });
+
+ if(!latch.await(timeout, timeUnit)) {
+ //timeout
+ throw new TimeoutException("Timeout after "+timeout+" "+timeUnit+" while retrieving configuration for "+Arrays.toString(events)+ "(index="+opendistrosecurityIndex+")");
+ }
+
+ return rs;
+ }
+
+ void loadAsyncLegacy(final String[] events, final ConfigCallback callback) {
+ if(events == null || events.length == 0) {
+ log.warn("No config events requested to load");
+ return;
+ }
+
+ final MultiGetRequest mget = new MultiGetRequest();
+
+ for (int i = 0; i < events.length; i++) {
+ final String event = events[i];
+ mget.add(opendistrosecurityIndex, event, "0");
+ }
+
+ mget.refresh(true);
+ mget.realtime(true);
+
+ //try(StoredContext ctx = threadContext.stashContext()) {
+ // threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true");
+ {
+ client.multiGet(mget, new ActionListener() {
+ @Override
+ public void onResponse(MultiGetResponse response) {
+ MultiGetItemResponse[] responses = response.getResponses();
+ for (int i = 0; i < responses.length; i++) {
+ MultiGetItemResponse singleResponse = responses[i];
+ if(singleResponse != null && !singleResponse.isFailed()) {
+ GetResponse singleGetResponse = singleResponse.getResponse();
+ if(singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) {
+ //success
+ final Settings _settings = toSettings(singleGetResponse.getSourceAsBytesRef(), singleGetResponse.getType());
+ if(_settings != null) {
+ callback.success(singleGetResponse.getType(), _settings);
+ } else {
+ log.error("Cannot parse settings for "+singleGetResponse.getType());
+ }
+ } else {
+ //does not exist or empty source
+ callback.noData(singleGetResponse.getType());
+ }
+ } else {
+ //failure
+ callback.singleFailure(singleResponse==null?null:singleResponse.getFailure());
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ callback.failure(e);
+ }
+ });
+ }
+ }
+
+ private Settings toSettings(final BytesReference ref, final String type) {
+ if (ref == null || ref.length() == 0) {
+ log.error("Empty or null byte reference for {}", type);
+ return null;
+ }
+
+ XContentParser parser = null;
+
+ try {
+ parser = XContentHelper.createParser(NamedXContentRegistry.EMPTY, OpenDistroSecurityDeprecationHandler.INSTANCE, ref, XContentType.JSON);
+ parser.nextToken();
+ parser.nextToken();
+
+ if(!type.equals((parser.currentName()))) {
+ log.error("Cannot parse config for type {} because {}!={}", type, type, parser.currentName());
+ return null;
+ }
+
+ parser.nextToken();
+
+ return Settings.builder().loadFromStream("dummy.json", new ByteArrayInputStream(parser.binaryValue()), true).build();
+ } catch (final IOException e) {
+ throw ExceptionsHelper.convertToElastic(e);
+ } finally {
+ if(parser != null) {
+ try {
+ parser.close();
+ } catch (IOException e) {
+ //ignore
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java
new file mode 100644
index 0000000000..c734952160
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/configuration/OpenDistroSecurityIndexSearcherWrapper.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.configuration;
+
+import java.io.IOException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.search.IndexSearcher;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.index.Index;
+import org.elasticsearch.index.IndexService;
+import org.elasticsearch.index.engine.EngineException;
+import org.elasticsearch.index.shard.IndexSearcherWrapper;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class OpenDistroSecurityIndexSearcherWrapper extends IndexSearcherWrapper {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ protected final ThreadContext threadContext;
+ protected final Index index;
+ protected final String opendistrosecurityIndex;
+ private final AdminDNs adminDns;
+
+ //constructor is called per index, so avoid costly operations here
+ public OpenDistroSecurityIndexSearcherWrapper(final IndexService indexService, final Settings settings, final AdminDNs adminDNs) {
+ index = indexService.index();
+ threadContext = indexService.getThreadPool().getThreadContext();
+ this.opendistrosecurityIndex = settings.get(ConfigConstants.OPENDISTRO_SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX);
+ this.adminDns = adminDNs;
+ }
+
+ @Override
+ public final DirectoryReader wrap(final DirectoryReader reader) throws IOException {
+
+ if (isSecurityIndexRequest() && !isAdminAuthenticatedOrInternalRequest()) {
+ return new EmptyFilterLeafReader.EmptyDirectoryReader(reader);
+ }
+
+
+ return dlsFlsWrap(reader, isAdminAuthenticatedOrInternalRequest());
+ }
+
+ @Override
+ public final IndexSearcher wrap(final IndexSearcher searcher) throws EngineException {
+ return dlsFlsWrap(searcher, isAdminAuthenticatedOrInternalRequest());
+ }
+
+ protected IndexSearcher dlsFlsWrap(final IndexSearcher searcher, boolean isAdmin) throws EngineException {
+ return searcher;
+ }
+
+ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdmin) throws IOException {
+ return reader;
+ }
+
+ protected final boolean isAdminAuthenticatedOrInternalRequest() {
+
+ final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
+
+ if (user != null && adminDns.isAdmin(user)) {
+ return true;
+ }
+
+ if ("true".equals(HeaderHelper.getSafeFromHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER))) {
+ return true;
+ }
+
+ return false;
+ }
+
+ protected final boolean isSecurityIndexRequest() {
+ return index.getName().equals(opendistrosecurityIndex);
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java
new file mode 100644
index 0000000000..aa1fdf584c
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityFilter.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.filter;
+
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.DocWriteRequest.OpType;
+import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest;
+import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest;
+import org.elasticsearch.action.admin.indices.close.CloseIndexRequest;
+import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
+import org.elasticsearch.action.bulk.BulkItemRequest;
+import org.elasticsearch.action.bulk.BulkRequest;
+import org.elasticsearch.action.bulk.BulkShardRequest;
+import org.elasticsearch.action.delete.DeleteRequest;
+import org.elasticsearch.action.get.GetRequest;
+import org.elasticsearch.action.get.MultiGetRequest;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.MultiSearchRequest;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.support.ActionFilter;
+import org.elasticsearch.action.support.ActionFilterChain;
+import org.elasticsearch.action.update.UpdateRequest;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
+import org.elasticsearch.index.reindex.DeleteByQueryRequest;
+import org.elasticsearch.index.reindex.UpdateByQueryRequest;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.action.whoami.WhoAmIAction;
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog;
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin;
+import com.amazon.opendistroforelasticsearch.security.compliance.ComplianceConfig;
+import com.amazon.opendistroforelasticsearch.security.configuration.AdminDNs;
+import com.amazon.opendistroforelasticsearch.security.configuration.CompatConfig;
+import com.amazon.opendistroforelasticsearch.security.configuration.DlsFlsRequestValve;
+import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluator;
+import com.amazon.opendistroforelasticsearch.security.privileges.PrivilegesEvaluatorResponse;
+import com.amazon.opendistroforelasticsearch.security.support.Base64Helper;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.HeaderHelper;
+import com.amazon.opendistroforelasticsearch.security.support.SourceFieldsContext;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class OpenDistroSecurityFilter implements ActionFilter {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ protected final Logger actionTrace = LogManager.getLogger("opendistro_security_action_trace");
+ private final PrivilegesEvaluator evalp;
+ private final AdminDNs adminDns;
+ private DlsFlsRequestValve dlsFlsValve;
+ private final AuditLog auditLog;
+ private final ThreadContext threadContext;
+ private final ClusterService cs;
+ private final ComplianceConfig complianceConfig;
+ private final CompatConfig compatConfig;
+
+ public OpenDistroSecurityFilter(final PrivilegesEvaluator evalp, final AdminDNs adminDns,
+ DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, ThreadPool threadPool, ClusterService cs,
+ ComplianceConfig complianceConfig, final CompatConfig compatConfig) {
+ this.evalp = evalp;
+ this.adminDns = adminDns;
+ this.dlsFlsValve = dlsFlsValve;
+ this.auditLog = auditLog;
+ this.threadContext = threadPool.getThreadContext();
+ this.cs = cs;
+ this.complianceConfig = complianceConfig;
+ this.compatConfig = compatConfig;
+ }
+
+ @Override
+ public int order() {
+ return Integer.MIN_VALUE;
+ }
+
+ @Override
+ public void apply(Task task, final String action, Request request,
+ ActionListener listener, ActionFilterChain chain) {
+ try (StoredContext ctx = threadContext.newStoredContext(true)){
+ org.apache.logging.log4j.ThreadContext.clearAll();
+ apply0(task, action, request, listener, chain);
+ }
+ }
+
+
+ private void apply0(Task task, final String action, Request request,
+ ActionListener listener, ActionFilterChain chain) {
+
+ try {
+
+ if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN) == null) {
+ threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.LOCAL.toString());
+ }
+
+ if(complianceConfig != null && complianceConfig.isEnabled()) {
+ attachSourceFieldContext(request);
+ }
+
+ final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER);
+ final boolean userIsAdmin = isUserAdmin(user, adminDns);
+ final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadContext);
+ final boolean trustedClusterRequest = HeaderHelper.isTrustedClusterRequest(threadContext);
+ final boolean confRequest = "true".equals(HeaderHelper.getSafeFromHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER));
+ final boolean passThroughRequest = action.startsWith("indices:admin/seq_no")
+ || action.equals(WhoAmIAction.NAME);
+
+ final boolean internalRequest =
+ (interClusterRequest || HeaderHelper.isDirectRequest(threadContext))
+ && action.startsWith("internal:")
+ && !action.startsWith("internal:transport/proxy");
+
+ if (user != null) {
+ org.apache.logging.log4j.ThreadContext.put("user", user.getName());
+ }
+
+ if(actionTrace.isTraceEnabled()) {
+
+ String count = "";
+ if(request instanceof BulkRequest) {
+ count = ""+((BulkRequest) request).requests().size();
+ }
+
+ if(request instanceof MultiGetRequest) {
+ count = ""+((MultiGetRequest) request).getItems().size();
+ }
+
+ if(request instanceof MultiSearchRequest) {
+ count = ""+((MultiSearchRequest) request).requests().size();
+ }
+
+ actionTrace.trace("Node "+cs.localNode().getName()+" -> "+action+" ("+count+"): userIsAdmin="+userIsAdmin+"/conRequest="+confRequest+"/internalRequest="+internalRequest
+ +"origin="+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+"/directRequest="+HeaderHelper.isDirectRequest(threadContext)+"/remoteAddress="+request.remoteAddress());
+
+
+ threadContext.putHeader("_opendistro_security_trace"+System.currentTimeMillis()+"#"+UUID.randomUUID().toString(), Thread.currentThread().getName()+" FILTER -> "+"Node "+cs.localNode().getName()+" -> "+action+" userIsAdmin="+userIsAdmin+"/conRequest="+confRequest+"/internalRequest="+internalRequest
+ +"origin="+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+"/directRequest="+HeaderHelper.isDirectRequest(threadContext)+"/remoteAddress="+request.remoteAddress()+" "+threadContext.getHeaders().entrySet().stream().filter(p->!p.getKey().startsWith("_opendistro_security_trace")).collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue())));
+
+
+ }
+
+
+ if(userIsAdmin
+ || confRequest
+ || internalRequest
+ || passThroughRequest){
+
+ if(userIsAdmin && !confRequest && !internalRequest && !passThroughRequest) {
+ auditLog.logGrantedPrivileges(action, request, task);
+ }
+
+ chain.proceed(task, action, request, listener);
+ return;
+ }
+
+
+ if(complianceConfig != null && complianceConfig.isEnabled()) {
+
+ boolean isImmutable = false;
+
+ if(request instanceof BulkShardRequest) {
+ for(BulkItemRequest bsr: ((BulkShardRequest) request).items()) {
+ isImmutable = checkImmutableIndices(bsr.request(), listener);
+ if(isImmutable) {
+ break;
+ }
+ }
+ } else {
+ isImmutable = checkImmutableIndices(request, listener);
+ }
+
+ if(isImmutable) {
+ return;
+ }
+
+ }
+
+ if(Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN))
+ && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext))
+ ) {
+
+ chain.proceed(task, action, request, listener);
+ return;
+ }
+
+ if(user == null) {
+
+ if(action.startsWith("cluster:monitor/state")) {
+ chain.proceed(task, action, request, listener);
+ return;
+ }
+
+ if((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null) && !compatConfig.transportInterClusterAuthEnabled()) {
+ chain.proceed(task, action, request, listener);
+ return;
+ }
+
+ log.error("No user found for "+ action+" from "+request.remoteAddress()+" "+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)+" via "+threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_CHANNEL_TYPE)+" "+threadContext.getHeaders());
+ listener.onFailure(new ElasticsearchSecurityException("No user found for "+action, RestStatus.INTERNAL_SERVER_ERROR));
+ return;
+ }
+
+ final PrivilegesEvaluator eval = evalp;
+
+ if (!eval.isInitialized()) {
+ log.error("Open Distro Security not initialized for {}", action);
+ listener.onFailure(new ElasticsearchSecurityException("Open Distro Security not initialized for "
+ + action, RestStatus.SERVICE_UNAVAILABLE));
+ return;
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("Evaluate permissions for user: {}", user.getName());
+ }
+
+ final PrivilegesEvaluatorResponse pres = eval.evaluate(user, action, request, task);
+
+ if (log.isDebugEnabled()) {
+ log.debug(pres);
+ }
+
+ if (pres.isAllowed()) {
+ auditLog.logGrantedPrivileges(action, request, task);
+ if(!dlsFlsValve.invoke(request, listener, pres.getAllowedFlsFields(), pres.getMaskedFields(), pres.getQueries())) {
+ return;
+ }
+ chain.proceed(task, action, request, listener);
+ return;
+ } else {
+ auditLog.logMissingPrivileges(action, request, task);
+ log.debug("no permissions for {}", pres.getMissingPrivileges());
+ listener.onFailure(new ElasticsearchSecurityException("no permissions for " + pres.getMissingPrivileges()+" and "+user, RestStatus.FORBIDDEN));
+ return;
+ }
+ } catch (Throwable e) {
+ log.error("Unexpected exception "+e, e);
+ listener.onFailure(new ElasticsearchSecurityException("Unexpected exception " + action, RestStatus.INTERNAL_SERVER_ERROR));
+ return;
+ }
+ }
+
+ private static boolean isUserAdmin(User user, final AdminDNs adminDns) {
+ if (user != null && adminDns.isAdmin(user)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void attachSourceFieldContext(ActionRequest request) {
+
+ if(request instanceof SearchRequest && SourceFieldsContext.isNeeded((SearchRequest) request)) {
+ if(threadContext.getHeader("_opendistro_security_source_field_context") == null) {
+ final String serializedSourceFieldContext = Base64Helper.serializeObject(new SourceFieldsContext((SearchRequest) request));
+ threadContext.putHeader("_opendistro_security_source_field_context", serializedSourceFieldContext);
+ }
+ } else if (request instanceof GetRequest && SourceFieldsContext.isNeeded((GetRequest) request)) {
+ if(threadContext.getHeader("_opendistro_security_source_field_context") == null) {
+ final String serializedSourceFieldContext = Base64Helper.serializeObject(new SourceFieldsContext((GetRequest) request));
+ threadContext.putHeader("_opendistro_security_source_field_context", serializedSourceFieldContext);
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ private boolean checkImmutableIndices(Object request, ActionListener listener) {
+
+ if( request instanceof DeleteRequest
+ || request instanceof UpdateRequest
+ || request instanceof UpdateByQueryRequest
+ || request instanceof DeleteByQueryRequest
+ || request instanceof DeleteIndexRequest
+ || request instanceof RestoreSnapshotRequest
+ || request instanceof CloseIndexRequest
+ || request instanceof IndicesAliasesRequest //TODO only remove index
+ ) {
+
+ if(complianceConfig != null && complianceConfig.isIndexImmutable(request)) {
+ //auditLog.log
+
+ //check index for type = remove index
+ //IndicesAliasesRequest iar = (IndicesAliasesRequest) request;
+ //for(AliasActions aa: iar.getAliasActions()) {
+ // if(aa.actionType() == Type.REMOVE_INDEX) {
+
+ // }
+ //}
+
+
+
+ listener.onFailure(new ElasticsearchSecurityException("Index is immutable", RestStatus.FORBIDDEN));
+ return true;
+ }
+ }
+
+ if(request instanceof IndexRequest) {
+ if(complianceConfig != null && complianceConfig.isIndexImmutable(request)) {
+ ((IndexRequest) request).opType(OpType.CREATE);
+ }
+ }
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java
new file mode 100644
index 0000000000..78c92616b9
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/filter/OpenDistroSecurityRestFilter.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.filter;
+
+import java.nio.file.Path;
+
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.rest.BytesRestResponse;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestHandler;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.RestRequest.Method;
+import org.elasticsearch.rest.RestStatus;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog;
+import com.amazon.opendistroforelasticsearch.security.auditlog.AuditLog.Origin;
+import com.amazon.opendistroforelasticsearch.security.auth.BackendRegistry;
+import com.amazon.opendistroforelasticsearch.security.configuration.CompatConfig;
+import com.amazon.opendistroforelasticsearch.security.ssl.transport.PrincipalExtractor;
+import com.amazon.opendistroforelasticsearch.security.ssl.util.ExceptionUtils;
+import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLRequestHelper;
+import com.amazon.opendistroforelasticsearch.security.ssl.util.SSLRequestHelper.SSLInfo;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.HTTPHelper;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class OpenDistroSecurityRestFilter {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private final BackendRegistry registry;
+ private final AuditLog auditLog;
+ private final ThreadContext threadContext;
+ private final PrincipalExtractor principalExtractor;
+ private final Settings settings;
+ private final Path configPath;
+ private final CompatConfig compatConfig;
+
+ public OpenDistroSecurityRestFilter(final BackendRegistry registry, final AuditLog auditLog,
+ final ThreadPool threadPool, final PrincipalExtractor principalExtractor,
+ final Settings settings, final Path configPath, final CompatConfig compatConfig) {
+ super();
+ this.registry = registry;
+ this.auditLog = auditLog;
+ this.threadContext = threadPool.getThreadContext();
+ this.principalExtractor = principalExtractor;
+ this.settings = settings;
+ this.configPath = configPath;
+ this.compatConfig = compatConfig;
+ }
+
+ public RestHandler wrap(RestHandler original) {
+ return new RestHandler() {
+
+ @Override
+ public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception {
+ org.apache.logging.log4j.ThreadContext.clearAll();
+ if(!checkAndAuthenticateRequest(request, channel, client)) {
+ original.handleRequest(request, channel, client);
+ }
+ }
+ };
+ }
+
+ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception {
+
+ threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString());
+
+ if(HTTPHelper.containsBadHeader(request)) {
+ final ElasticsearchException exception = ExceptionUtils.createBadHeaderException();
+ log.error(exception);
+ auditLog.logBadHeaders(request);
+ channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception));
+ return true;
+ }
+
+ if(SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) {
+ final ElasticsearchException exception = ExceptionUtils.createBadHeaderException();
+ log.error(exception);
+ auditLog.logBadHeaders(request);
+ channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, exception));
+ return true;
+ }
+
+ final SSLInfo sslInfo;
+ try {
+ if((sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor)) != null) {
+ if(sslInfo.getPrincipal() != null) {
+ threadContext.putTransient("_opendistro_security_ssl_principal", sslInfo.getPrincipal());
+ }
+
+ if(sslInfo.getX509Certs() != null) {
+ threadContext.putTransient("_opendistro_security_ssl_peer_certificates", sslInfo.getX509Certs());
+ }
+ threadContext.putTransient("_opendistro_security_ssl_protocol", sslInfo.getProtocol());
+ threadContext.putTransient("_opendistro_security_ssl_cipher", sslInfo.getCipher());
+ }
+ } catch (SSLPeerUnverifiedException e) {
+ log.error("No ssl info", e);
+ auditLog.logSSLException(request, e);
+ channel.sendResponse(new BytesRestResponse(channel, RestStatus.FORBIDDEN, e));
+ return true;
+ }
+
+ if(!compatConfig.restAuthEnabled()) {
+ return false;
+ }
+
+ if(request.method() != Method.OPTIONS
+ && !"/_opendistro/_security/health".equals(request.path())) {
+ if (!registry.authenticate(request, channel, threadContext)) {
+ // another roundtrip
+ org.apache.logging.log4j.ThreadContext.remove("user");
+ return true;
+ } else {
+ // make it possible to filter logs by username
+ org.apache.logging.log4j.ThreadContext.put("user", ((User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)).getName());
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java
new file mode 100644
index 0000000000..9b031132a3
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPBasicAuthenticator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import java.nio.file.Path;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.rest.BytesRestResponse;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.RestStatus;
+
+import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator;
+import com.amazon.opendistroforelasticsearch.security.support.HTTPHelper;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+
+//TODO FUTURE allow only if protocol==https
+public class HTTPBasicAuthenticator implements HTTPAuthenticator {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+
+ public HTTPBasicAuthenticator(final Settings settings, final Path configPath) {
+
+ }
+
+ @Override
+ public AuthCredentials extractCredentials(final RestRequest request, ThreadContext threadContext) {
+
+ final boolean forceLogin = request.paramAsBoolean("force_login", false);
+
+ if(forceLogin) {
+ return null;
+ }
+
+ final String authorizationHeader = request.header("Authorization");
+
+ return HTTPHelper.extractCredentials(authorizationHeader, log);
+ }
+
+ @Override
+ public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) {
+ final BytesRestResponse wwwAuthenticateResponse = new BytesRestResponse(RestStatus.UNAUTHORIZED, "Unauthorized");
+ wwwAuthenticateResponse.addHeader("WWW-Authenticate", "Basic realm=\"Open Distro Security\"");
+ channel.sendResponse(wwwAuthenticateResponse);
+ return true;
+ }
+
+ @Override
+ public String getType() {
+ return "basic";
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java
new file mode 100644
index 0000000000..1b2ab117b9
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPClientCertAuthenticator.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestRequest;
+
+import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+
+public class HTTPClientCertAuthenticator implements HTTPAuthenticator {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ protected final Settings settings;
+
+ public HTTPClientCertAuthenticator(final Settings settings, final Path configPath) {
+ this.settings = settings;
+ }
+
+ @Override
+ public AuthCredentials extractCredentials(final RestRequest request, final ThreadContext threadContext) {
+
+ final String principal = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL);
+
+ if (!Strings.isNullOrEmpty(principal)) {
+
+ final String usernameAttribute = settings.get("username_attribute");
+ final String rolesAttribute = settings.get("roles_attribute");
+
+ try {
+ final LdapName rfc2253dn = new LdapName(principal);
+ String username = principal.trim();
+ String[] backendRoles = null;
+
+ if(usernameAttribute != null && usernameAttribute.length() > 0) {
+ final List usernames = getDnAttribute(rfc2253dn, usernameAttribute);
+ if(usernames.isEmpty() == false) {
+ username = usernames.get(0);
+ }
+ }
+
+ if(rolesAttribute != null && rolesAttribute.length() > 0) {
+ final List roles = getDnAttribute(rfc2253dn, rolesAttribute);
+ if(roles.isEmpty() == false) {
+ backendRoles = roles.toArray(new String[0]);
+ }
+ }
+
+ return new AuthCredentials(username, backendRoles).markComplete();
+ } catch (InvalidNameException e) {
+ log.error("Client cert had no properly formed DN (was: {})", principal);
+ return null;
+ }
+
+ } else {
+ log.trace("No CLIENT CERT, send 401");
+ return null;
+ }
+ }
+
+ @Override
+ public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) {
+ return false;
+ }
+
+ @Override
+ public String getType() {
+ return "clientcert";
+ }
+
+ private List getDnAttribute(LdapName rfc2253dn, String attribute) {
+ final List attrValues = new ArrayList<>(rfc2253dn.size());
+ final List reverseRdn = new ArrayList<>(rfc2253dn.getRdns());
+ Collections.reverse(reverseRdn);
+
+ for (Rdn rdn : reverseRdn) {
+ if (rdn.getType().equalsIgnoreCase(attribute)) {
+ attrValues.add(rdn.getValue().toString());
+ }
+ }
+
+ return Collections.unmodifiableList(attrValues);
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java
new file mode 100644
index 0000000000..00bf8b24c8
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/HTTPProxyAuthenticator.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import java.nio.file.Path;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.rest.RestChannel;
+import org.elasticsearch.rest.RestRequest;
+
+import com.amazon.opendistroforelasticsearch.security.auth.HTTPAuthenticator;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.user.AuthCredentials;
+
+public class HTTPProxyAuthenticator implements HTTPAuthenticator {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private volatile Settings settings;
+
+ public HTTPProxyAuthenticator(Settings settings, final Path configPath) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) {
+
+ if(context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) != Boolean.TRUE) {
+ throw new ElasticsearchSecurityException("xff not done");
+ }
+
+ final String userHeader = settings.get("user_header");
+ final String rolesHeader = settings.get("roles_header");
+ final String rolesSeparator = settings.get("roles_separator", ",");
+
+ if(log.isDebugEnabled()) {
+ log.debug("headers {}", request.getHeaders());
+ log.debug("userHeader {}, value {}", userHeader, userHeader == null?null:request.header(userHeader));
+ log.debug("rolesHeader {}, value {}", rolesHeader, rolesHeader == null?null:request.header(rolesHeader));
+ }
+
+ if (!Strings.isNullOrEmpty(userHeader) && !Strings.isNullOrEmpty((String) request.header(userHeader))) {
+
+ String[] backendRoles = null;
+
+ if (!Strings.isNullOrEmpty(rolesHeader) && !Strings.isNullOrEmpty((String) request.header(rolesHeader))) {
+ backendRoles = ((String) request.header(rolesHeader)).split(rolesSeparator);
+ }
+ return new AuthCredentials((String) request.header(userHeader), backendRoles).markComplete();
+ } else {
+ if(log.isTraceEnabled()) {
+ log.trace("No '{}' header, send 401", userHeader);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public boolean reRequestAuthentication(final RestChannel channel, AuthCredentials creds) {
+ return false;
+ }
+
+ @Override
+ public String getType() {
+ return "proxy";
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java
new file mode 100644
index 0000000000..3089c2b10b
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityHttpServerTransport.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import org.elasticsearch.common.network.NetworkService;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.ssl.OpenDistroSecurityKeyStore;
+import com.amazon.opendistroforelasticsearch.security.ssl.SslExceptionHandler;
+import com.amazon.opendistroforelasticsearch.security.ssl.http.netty.OpenDistroSecuritySSLNettyHttpServerTransport;
+import com.amazon.opendistroforelasticsearch.security.ssl.http.netty.ValidatingDispatcher;
+
+public class OpenDistroSecurityHttpServerTransport extends OpenDistroSecuritySSLNettyHttpServerTransport {
+
+ public OpenDistroSecurityHttpServerTransport(final Settings settings, final NetworkService networkService,
+ final BigArrays bigArrays, final ThreadPool threadPool, final OpenDistroSecurityKeyStore odsks,
+ final SslExceptionHandler sslExceptionHandler, final NamedXContentRegistry namedXContentRegistry, final ValidatingDispatcher dispatcher) {
+ super(settings, networkService, bigArrays, threadPool, odsks, namedXContentRegistry, dispatcher, sslExceptionHandler);
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java
new file mode 100644
index 0000000000..d4e1ae2879
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/OpenDistroSecurityNonSslHttpServerTransport.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+
+import org.elasticsearch.common.network.NetworkService;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.BigArrays;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.http.netty4.Netty4HttpServerTransport;
+import org.elasticsearch.threadpool.ThreadPool;
+
+public class OpenDistroSecurityNonSslHttpServerTransport extends Netty4HttpServerTransport {
+
+ private final ThreadContext threadContext;
+
+ public OpenDistroSecurityNonSslHttpServerTransport(final Settings settings, final NetworkService networkService, final BigArrays bigArrays,
+ final ThreadPool threadPool, final NamedXContentRegistry namedXContentRegistry, final Dispatcher dispatcher) {
+ super(settings, networkService, bigArrays, threadPool, namedXContentRegistry, dispatcher);
+ this.threadContext = threadPool.getThreadContext();
+ }
+
+ @Override
+ public ChannelHandler configureServerChannelHandler() {
+ return new NonSslHttpChannelHandler(this);
+ }
+
+ protected class NonSslHttpChannelHandler extends Netty4HttpServerTransport.HttpChannelHandler {
+
+ protected NonSslHttpChannelHandler(Netty4HttpServerTransport transport) {
+ super(transport, OpenDistroSecurityNonSslHttpServerTransport.this.detailedErrorsEnabled, OpenDistroSecurityNonSslHttpServerTransport.this.threadContext);
+ }
+
+ @Override
+ protected void initChannel(Channel ch) throws Exception {
+ super.initChannel(ch);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java
new file mode 100644
index 0000000000..bdc6c956e7
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/RemoteIpDetector.java
@@ -0,0 +1,343 @@
+/*
+ * 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.
+ */
+
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import java.net.InetSocketAddress;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.http.netty4.Netty4HttpRequest;
+
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+
+class RemoteIpDetector {
+
+ /**
+ * {@link Pattern} for a comma delimited string that support whitespace characters
+ */
+ private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*");
+
+ /**
+ * Logger
+ */
+ protected final Logger log = LogManager.getLogger(this.getClass());
+
+ /**
+ * Convert a given comma delimited String into an array of String
+ *
+ * @return array of String (non null
)
+ */
+ protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) {
+ return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern
+ .split(commaDelimitedStrings);
+ }
+
+ /**
+ * Convert an array of strings in a comma delimited string
+ */
+ protected static String listToCommaDelimitedString(List stringList) {
+ if (stringList == null) {
+ return "";
+ }
+ StringBuilder result = new StringBuilder();
+ for (Iterator it = stringList.iterator(); it.hasNext();) {
+ Object element = it.next();
+ if (element != null) {
+ result.append(element);
+ if (it.hasNext()) {
+ result.append(", ");
+ }
+ }
+ }
+ return result.toString();
+ }
+
+ /**
+ * @see #setInternalProxies(String)
+ */
+ private Pattern internalProxies = Pattern.compile(
+ "10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
+ "192\\.168\\.\\d{1,3}\\.\\d{1,3}|" +
+ "169\\.254\\.\\d{1,3}\\.\\d{1,3}|" +
+ "127\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" +
+ "172\\.1[6-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" +
+ "172\\.2[0-9]{1}\\.\\d{1,3}\\.\\d{1,3}|" +
+ "172\\.3[0-1]{1}\\.\\d{1,3}\\.\\d{1,3}");
+
+ /**
+ * @see #setProxiesHeader(String)
+ */
+ private String proxiesHeader = "X-Forwarded-By";
+
+ /**
+ * @see #setRemoteIpHeader(String)
+ */
+ private String remoteIpHeader = "X-Forwarded-For";
+
+ /**
+ * @see RemoteIpValve#setTrustedProxies(String)
+ */
+ private Pattern trustedProxies = null;
+
+ /**
+ * @see #setInternalProxies(String)
+ * @return Regular expression that defines the internal proxies
+ */
+ public String getInternalProxies() {
+ if (internalProxies == null) {
+ return null;
+ }
+ return internalProxies.toString();
+ }
+
+ /**
+ * @see #setProxiesHeader(String)
+ * @return the proxies header name (e.g. "X-Forwarded-By")
+ */
+ public String getProxiesHeader() {
+ return proxiesHeader;
+ }
+
+ /**
+ * @see #setRemoteIpHeader(String)
+ * @return the remote IP header name (e.g. "X-Forwarded-For")
+ */
+ public String getRemoteIpHeader() {
+ return remoteIpHeader;
+ }
+
+ /**
+ * @see #setTrustedProxies(String)
+ * @return Regular expression that defines the trusted proxies
+ */
+ public String getTrustedProxies() {
+ if (trustedProxies == null) {
+ return null;
+ }
+ return trustedProxies.toString();
+ }
+
+ String detect(final Netty4HttpRequest request, ThreadContext threadContext){
+ final String originalRemoteAddr = ((InetSocketAddress)request.getRemoteAddress()).getAddress().getHostAddress();
+ @SuppressWarnings("unused")
+ final String originalProxiesHeader = request.header(proxiesHeader);
+ //final String originalRemoteIpHeader = request.getHeader(remoteIpHeader);
+
+ if(log.isTraceEnabled()) {
+ log.trace("originalRemoteAddr {}", originalRemoteAddr);
+ }
+
+ //X-Forwarded-For: client1, proxy1, proxy2
+ // ^^^^^^ originalRemoteAddr
+
+ //originalRemoteAddr need to be in the list of internalProxies
+ if (internalProxies !=null &&
+ internalProxies.matcher(originalRemoteAddr).matches()) {
+ String remoteIp = null;
+ // In java 6, proxiesHeaderValue should be declared as a java.util.Deque
+ final LinkedList proxiesHeaderValue = new LinkedList<>();
+ final StringBuilder concatRemoteIpHeaderValue = new StringBuilder();
+
+ //client1, proxy1, proxy2
+ final List remoteIpHeaders = request.request().headers().getAll(remoteIpHeader); //X-Forwarded-For
+
+ if(remoteIpHeaders == null || remoteIpHeaders.isEmpty()) {
+ return originalRemoteAddr;
+ }
+
+ for (String rh:remoteIpHeaders) {
+ if (concatRemoteIpHeaderValue.length() > 0) {
+ concatRemoteIpHeaderValue.append(", ");
+ }
+
+ concatRemoteIpHeaderValue.append(rh);
+ }
+
+ if(log.isTraceEnabled()) {
+ log.trace("concatRemoteIpHeaderValue {}", concatRemoteIpHeaderValue.toString());
+ }
+
+ final String[] remoteIpHeaderValue = commaDelimitedListToStringArray(concatRemoteIpHeaderValue.toString());
+ int idx;
+ // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain
+ for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) {
+ String currentRemoteIp = remoteIpHeaderValue[idx];
+ remoteIp = currentRemoteIp;
+ if (internalProxies.matcher(currentRemoteIp).matches()) {
+ // do nothing, internalProxies IPs are not appended to the
+ } else if (trustedProxies != null &&
+ trustedProxies.matcher(currentRemoteIp).matches()) {
+ proxiesHeaderValue.addFirst(currentRemoteIp);
+ } else {
+ idx--; // decrement idx because break statement doesn't do it
+ break;
+ }
+ }
+
+ // continue to loop on remoteIpHeaderValue to build the new value of the remoteIpHeader
+ final LinkedList newRemoteIpHeaderValue = new LinkedList<>();
+ for (; idx >= 0; idx--) {
+ String currentRemoteIp = remoteIpHeaderValue[idx];
+ newRemoteIpHeaderValue.addFirst(currentRemoteIp);
+ }
+
+ if (remoteIp != null) {
+
+ if (proxiesHeaderValue.size() == 0) {
+ request.request().headers().remove(proxiesHeader);
+ } else {
+ String commaDelimitedListOfProxies = listToCommaDelimitedString(proxiesHeaderValue);
+ request.request().headers().set(proxiesHeader,commaDelimitedListOfProxies);
+ }
+ if (newRemoteIpHeaderValue.size() == 0) {
+ request.request().headers().remove(remoteIpHeader);
+ } else {
+ String commaDelimitedRemoteIpHeaderValue = listToCommaDelimitedString(newRemoteIpHeaderValue);
+ request.request().headers().set(remoteIpHeader,commaDelimitedRemoteIpHeaderValue);
+ }
+
+ if (log.isTraceEnabled()) {
+ final String originalRemoteHost = ((InetSocketAddress)request.getRemoteAddress()).getAddress().getHostName();
+ log.trace("Incoming request " + request.request().uri() + " with originalRemoteAddr '" + originalRemoteAddr
+ + "', originalRemoteHost='" + originalRemoteHost + "', will be seen as newRemoteAddr='" + remoteIp);
+ }
+
+ //TODO check put in thread context
+ threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE, Boolean.TRUE);
+ //request.putInContext(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE, Boolean.TRUE);
+ return remoteIp;
+
+ } else {
+ log.warn("Remote ip could not be detected, this should normally not happen");
+ }
+
+ } else {
+ if (log.isTraceEnabled()) {
+ log.trace("Skip RemoteIpDetector for request " + request.request().uri() + " with originalRemoteAddr '"
+ + request.getRemoteAddress() + "' cause no internal proxy matches");
+ }
+ }
+
+ return originalRemoteAddr;
+ }
+
+ /**
+ *
+ * Regular expression that defines the internal proxies.
+ *
+ *
+ * Default value : 10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|169\.254.\d{1,3}.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}
+ *
+ */
+ public void setInternalProxies(String internalProxies) {
+ if (internalProxies == null || internalProxies.length() == 0) {
+ this.internalProxies = null;
+ } else {
+ this.internalProxies = Pattern.compile(internalProxies);
+ }
+ }
+
+ /**
+ *
+ * The proxiesHeader directive specifies a header into which mod_remoteip will collect a list of all of the intermediate client IP
+ * addresses trusted to resolve the actual remote IP. Note that intermediate RemoteIPTrustedProxy addresses are recorded in this header,
+ * while any intermediate RemoteIPInternalProxy addresses are discarded.
+ *
+ *
+ * Name of the http header that holds the list of trusted proxies that has been traversed by the http request.
+ *
+ *
+ * The value of this header can be comma delimited.
+ *
+ *
+ * Default value : X-Forwarded-By
+ *
+ */
+ public void setProxiesHeader(String proxiesHeader) {
+ this.proxiesHeader = proxiesHeader;
+ }
+
+ /**
+ *
+ * Name of the http header from which the remote ip is extracted.
+ *
+ *
+ * The value of this header can be comma delimited.
+ *
+ *
+ * Default value : X-Forwarded-For
+ *
+ *
+ * @param remoteIpHeader
+ */
+ public void setRemoteIpHeader(String remoteIpHeader) {
+ this.remoteIpHeader = remoteIpHeader;
+ }
+
+ /**
+ *
+ * Regular expression defining proxies that are trusted when they appear in
+ * the {@link #remoteIpHeader} header.
+ *
+ *
+ * Default value : empty list, no external proxy is trusted.
+ *
+ */
+ public void setTrustedProxies(String trustedProxies) {
+ if (trustedProxies == null || trustedProxies.length() == 0) {
+ this.trustedProxies = null;
+ } else {
+ this.trustedProxies = Pattern.compile(trustedProxies);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java
new file mode 100644
index 0000000000..7b7ab0a5f8
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/http/XFFResolver.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.http;
+
+import java.net.InetSocketAddress;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.TransportAddress;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.http.netty4.Netty4HttpRequest;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.configuration.ConfigurationChangeListener;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+
+public class XFFResolver implements ConfigurationChangeListener {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+ private volatile boolean enabled;
+ private volatile RemoteIpDetector detector;
+ private final ThreadContext threadContext;
+
+ public XFFResolver(final ThreadPool threadPool) {
+ super();
+ this.threadContext = threadPool.getThreadContext();
+ }
+
+ public TransportAddress resolve(final RestRequest request) throws ElasticsearchSecurityException {
+
+ if(log.isTraceEnabled()) {
+ log.trace("resolve {}", request.getRemoteAddress());
+ }
+
+ if(enabled && request.getRemoteAddress() instanceof InetSocketAddress && request instanceof Netty4HttpRequest) {
+
+ final InetSocketAddress isa = new InetSocketAddress(detector.detect((Netty4HttpRequest) request, threadContext), ((InetSocketAddress)request.getRemoteAddress()).getPort());
+
+ if(isa.isUnresolved()) {
+ throw new ElasticsearchSecurityException("Cannot resolve address "+isa.getHostString());
+ }
+
+
+ if(log.isTraceEnabled()) {
+ if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) == Boolean.TRUE) {
+ log.trace("xff resolved {} to {}", request.getRemoteAddress(), isa);
+ } else {
+ log.trace("no xff done for {}",request.getClass());
+ }
+ }
+ return new TransportAddress(isa);
+ } else if(request.getRemoteAddress() instanceof InetSocketAddress){
+
+ if(log.isTraceEnabled()) {
+ log.trace("no xff done (enabled or no netty request) {},{},{},{}",enabled, request.getClass());
+
+ }
+ return new TransportAddress((InetSocketAddress)request.getRemoteAddress());
+ } else {
+ throw new ElasticsearchSecurityException("Cannot handle this request. Remote address is "+request.getRemoteAddress()+" with request class "+request.getClass());
+ }
+ }
+
+ @Override
+ public void onChange(final Settings settings) {
+ enabled = settings.getAsBoolean("opendistro_security.dynamic.http.xff.enabled", true);
+ if(enabled) {
+ detector = new RemoteIpDetector();
+ detector.setInternalProxies(settings.get("opendistro_security.dynamic.http.xff.internalProxies", detector.getInternalProxies()));
+ detector.setProxiesHeader(settings.get("opendistro_security.dynamic.http.xff.proxiesHeader", detector.getProxiesHeader()));
+ detector.setRemoteIpHeader(settings.get("opendistro_security.dynamic.http.xff.remoteIpHeader", detector.getRemoteIpHeader()));
+ detector.setTrustedProxies(settings.get("opendistro_security.dynamic.http.xff.trustedProxies", detector.getTrustedProxies()));
+ } else {
+ detector = null;
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java
new file mode 100644
index 0000000000..00f6ae38cb
--- /dev/null
+++ b/src/main/java/com/amazon/opendistroforelasticsearch/security/privileges/DlsFlsEvaluator.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2015-2018 _floragunn_ GmbH
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * Portions Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file 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 com.amazon.opendistroforelasticsearch.security.privileges;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
+import org.elasticsearch.cluster.service.ClusterService;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.util.concurrent.ThreadContext;
+import org.elasticsearch.threadpool.ThreadPool;
+
+import com.amazon.opendistroforelasticsearch.security.resolver.IndexResolverReplacer.Resolved;
+import com.amazon.opendistroforelasticsearch.security.securityconf.ConfigModel.SecurityRoles;
+import com.amazon.opendistroforelasticsearch.security.support.Base64Helper;
+import com.amazon.opendistroforelasticsearch.security.support.ConfigConstants;
+import com.amazon.opendistroforelasticsearch.security.support.WildcardMatcher;
+import com.amazon.opendistroforelasticsearch.security.user.User;
+
+public class DlsFlsEvaluator {
+
+ protected final Logger log = LogManager.getLogger(this.getClass());
+
+ private final ThreadPool threadPool;
+
+ public DlsFlsEvaluator(Settings settings, ThreadPool threadPool) {
+ this.threadPool = threadPool;
+ }
+
+ public PrivilegesEvaluatorResponse evaluate(final ClusterService clusterService, final IndexNameExpressionResolver resolver, final Resolved requestedResolved, final User user,
+ final SecurityRoles securityRoles, final PrivilegesEvaluatorResponse presponse) {
+
+ ThreadContext threadContext = threadPool.getThreadContext();
+
+ // maskedFields
+ final Map> maskedFieldsMap = securityRoles.getMaskedFields(user, resolver, clusterService);
+
+ if (maskedFieldsMap != null && !maskedFieldsMap.isEmpty()) {
+ if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER) != null) {
+ if (!maskedFieldsMap.equals(Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER)))) {
+ throw new ElasticsearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " does not match (Security 901D)");
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " already set");
+ }
+ }
+ } else {
+ threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, Base64Helper.serializeObject((Serializable) maskedFieldsMap));
+ if (log.isDebugEnabled()) {
+ log.debug("attach masked fields info: {}", maskedFieldsMap);
+ }
+ }
+
+ presponse.maskedFields = new HashMap<>(maskedFieldsMap);
+
+ if (!requestedResolved.getAllIndices().isEmpty()) {
+ for (Iterator>> it = presponse.maskedFields.entrySet().iterator(); it.hasNext();) {
+ Entry> entry = it.next();
+ if (!WildcardMatcher.matchAny(entry.getKey(), requestedResolved.getAllIndices(), false)) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+
+
+ // attach dls/fls map if not already done
+ // TODO do this only if enterprise module are loaded
+ final Tuple