Commit d1f7609a authored by Nils Bandener's avatar Nils Bandener
Browse files

Ported REST authcz process to async operation

This changes the authcz process from synchronous operation to callback-based async operation. This allows to perform ES operations during the authcz process.
parent f3c52c4c
......@@ -124,6 +124,7 @@ import com.floragunn.searchguard.auditlog.AuditLog;
import com.floragunn.searchguard.auditlog.AuditLog.Origin;
import com.floragunn.searchguard.auditlog.AuditLogSslExceptionHandler;
import com.floragunn.searchguard.auditlog.NullAuditLog;
import com.floragunn.searchguard.auth.AuthInfoService;
import com.floragunn.searchguard.auth.BackendRegistry;
import com.floragunn.searchguard.compliance.ComplianceConfig;
import com.floragunn.searchguard.compliance.ComplianceIndexingOperationListener;
......@@ -210,6 +211,7 @@ public final class SearchGuardPlugin extends SearchGuardSSLPlugin implements Clu
private SearchGuardModulesRegistry moduleRegistry;
private StaticSgConfig staticSgConfig;
private AuthInfoService authInfoService;
@Override
public void close() throws IOException {
......@@ -817,8 +819,8 @@ public final class SearchGuardPlugin extends SearchGuardSSLPlugin implements Clu
backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool);
final CompatConfig compatConfig = new CompatConfig(environment);
evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, indexNameExpressionResolver, auditLog, settings, privilegesInterceptor, cih, irr,
enterpriseModulesEnabled);
evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, indexNameExpressionResolver, auditLog, settings, privilegesInterceptor,
cih, irr, specialPrivilegesEvaluationContextProviderRegistry, enterpriseModulesEnabled);
final DynamicConfigFactory dcf = new DynamicConfigFactory(cr, staticSgConfig, settings, configPath, localClient, threadPool, cih, moduleRegistry);
dcf.registerDCFListener(backendRegistry);
......@@ -834,7 +836,7 @@ public final class SearchGuardPlugin extends SearchGuardSSLPlugin implements Clu
InternalAuthTokenProvider internalAuthTokenProvider = new InternalAuthTokenProvider(dcf);
specialPrivilegesEvaluationContextProviderRegistry.add(internalAuthTokenProvider::userAuthFromToken);
authInfoService = new AuthInfoService(threadPool, specialPrivilegesEvaluationContextProviderRegistry);
ResourceOwnerService resourceOwnerService = new ResourceOwnerService(localClient, clusterService, threadPool, protectedIndices, settings);
ExtendedActionHandlingService extendedActionHandlingService = new ExtendedActionHandlingService(resourceOwnerService, settings);
......@@ -855,7 +857,7 @@ public final class SearchGuardPlugin extends SearchGuardSSLPlugin implements Clu
components.add(principalExtractor);
protectedConfigIndexService = new ProtectedConfigIndexService(localClient, clusterService, threadPool, protectedIndices);
components.add(adminDns);
components.add(cr);
components.add(xffResolver);
......@@ -867,6 +869,7 @@ public final class SearchGuardPlugin extends SearchGuardSSLPlugin implements Clu
components.add(moduleRegistry);
components.add(protectedConfigIndexService);
components.add(staticSgConfig);
components.add(authInfoService);
BaseDependencies baseDependencies = new BaseDependencies(settings, localClient, clusterService, threadPool, resourceWatcherService,
scriptService, xContentRegistry, environment, nodeEnvironment, indexNameExpressionResolver, dcf, staticSgConfig, cr,
......
/*
* Copyright 2021 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.
*
*/
package com.floragunn.searchguard.auth;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.threadpool.ThreadPool;
import com.floragunn.searchguard.privileges.SpecialPrivilegesEvaluationContext;
import com.floragunn.searchguard.privileges.SpecialPrivilegesEvaluationContextProviderRegistry;
import com.floragunn.searchguard.support.ConfigConstants;
import com.floragunn.searchguard.user.User;
public class AuthInfoService {
private final ThreadPool threadPool;
private final SpecialPrivilegesEvaluationContextProviderRegistry specialPrivilegesEvaluationContextProviderRegistry;
public AuthInfoService(ThreadPool threadPool, SpecialPrivilegesEvaluationContextProviderRegistry specialPrivilegesEvaluationContextProviderRegistry) {
this.threadPool = threadPool;
this.specialPrivilegesEvaluationContextProviderRegistry = specialPrivilegesEvaluationContextProviderRegistry;
}
public User getCurrentUser() {
User user = peekCurrentUser();
if (user == null) {
throw new ElasticsearchSecurityException("No user information available");
}
return user;
}
public User peekCurrentUser() {
return threadPool.getThreadContext().getTransient(ConfigConstants.SG_USER);
}
public SpecialPrivilegesEvaluationContext getSpecialPrivilegesEvaluationContext() {
return specialPrivilegesEvaluationContextProviderRegistry.provide(getCurrentUser(), threadPool.getThreadContext());
}
}
/*
* Copyright 2015-2020 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.
*
*/
package com.floragunn.searchguard.auth;
import org.elasticsearch.rest.RestStatus;
import com.floragunn.searchguard.user.User;
public class AuthczResult {
public static final AuthczResult STOP = new AuthczResult(Status.STOP);
public static final AuthczResult PASS_ANONYMOUS = new AuthczResult(User.ANONYMOUS, Status.PASS);
public static final AuthczResult PASS_WITHOUT_AUTH = new AuthczResult(null, Status.PASS);
public static AuthczResult stop(RestStatus restStatus, String message) {
return new AuthczResult(Status.STOP, restStatus, message);
}
public static AuthczResult pass(User user) {
return new AuthczResult(user, Status.PASS);
}
private final User user;
private final Status status;
private final RestStatus restStatus;
private final String restStatusMessage;
public AuthczResult(User user, Status status) {
this.user = user;
this.status = status;
this.restStatus = null;
this.restStatusMessage = null;
}
public AuthczResult(Status status) {
this.user = null;
this.status = status;
this.restStatus = null;
this.restStatusMessage = null;
}
public AuthczResult(Status status, RestStatus restStatus, String restStatusMessage) {
this.user = null;
this.status = status;
this.restStatus = restStatus;
this.restStatusMessage = restStatusMessage;
}
public static enum Status {
PASS, STOP
}
public User getUser() {
return user;
}
public Status getStatus() {
return status;
}
public RestStatus getRestStatus() {
return restStatus;
}
public String getRestStatusMessage() {
return restStatusMessage;
}
}
/*
* Copyright 2015-2017 floragunn GmbH
* Copyright 2015-2021 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
......@@ -12,63 +12,16 @@
* WITHOUT 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.floragunn.searchguard.auth;
import org.elasticsearch.ElasticsearchSecurityException;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.User;
import com.floragunn.searchguard.auth.api.SyncAuthenticationBackend;
/**
* Search Guard custom authentication backends need to implement this interface.
* <p/>
* Authentication backends verify {@link AuthCredentials} and, if successfully verified, return a {@link User}.
* <p/>
* Implementation classes must provide a public constructor
* <p/>
* {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)}
* <p/>
* 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:
* <p/>
* {@code private final Logger log = LogManager.getLogger(this.getClass());}
*
* <p/>
* <b>Custom authenticators is a commercial feature. To make them work you need to obtain a license here:
* https://floragunn.com
* </b>
* @deprecated Please use now one of the classes AuthenticationBackend or SyncAuthenticationBackend from the package com.floragunn.searchguard.auth.api
*/
public interface AuthenticationBackend {
/**
* The type (name) of the authenticator. Only for logging.
* @return the type
*/
String getType();
/**
* Validate credentials and return an authenticated user (or throw an ElasticsearchSecurityException)
* <p/>
* Results of this method are normally cached so that we not need to query the backend for every authentication attempt.
* <p/>
* @param The credentials to be validated, never null
* @return the authenticated User, never null
* @throws ElasticsearchSecurityException in case an authentication failure
* (when credentials are incorrect, the user does not exist or the backend is not reachable)
*/
User authenticate(AuthCredentials credentials) throws ElasticsearchSecurityException;
/**
*
* Lookup for a specific user in the authentication backend
*
* @param user The user for which the authentication backend should be queried. If the authentication backend supports
* user attributes in combination with impersonation the attributes needs to be added to user by calling {@code user.addAttributes()}
* @return true if the user exists in the authentication backend, false otherwise. Before return call {@code user.addAttributes()} as explained above.
*/
boolean exists(User user);
public interface AuthenticationBackend extends SyncAuthenticationBackend {
}
......@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.floragunn.searchguard.auth.api.AuthenticationBackend;
import com.floragunn.searchguard.support.IPAddressCollection;
public class AuthenticationDomain implements Comparable<AuthenticationDomain> {
......
/*
* Copyright 2015-2017 floragunn GmbH
* Copyright 2015-2021 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
......@@ -12,54 +12,16 @@
* WITHOUT 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.floragunn.searchguard.auth;
import org.elasticsearch.ElasticsearchSecurityException;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.User;
import com.floragunn.searchguard.auth.api.SyncAuthorizationBackend;
/**
* Search Guard custom authorization backends need to implement this interface.
* <p/>
* Authorization backends populate a prior authenticated {@link User} with backend roles who's the user is a member of.
* <p/>
* Implementation classes must provide a public constructor
* <p/>
* {@code public MyHTTPAuthenticator(org.elasticsearch.common.settings.Settings settings, java.nio.file.Path configPath)}
* <p/>
* 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:
* <p/>
* {@code private final Logger log = LogManager.getLogger(this.getClass());}
*
* <p/>
* <b>Custom authorizers is a commercial feature. To make them work you need to obtain a license here:
* https://floragunn.com
* </b>
* @deprecated Please use now one of the classes AuthorizationBackend or SyncAuthorizationBackend from the package com.floragunn.searchguard.auth.api
*/
public interface AuthorizationBackend {
/**
* The type (name) of the authorizer. Only for logging.
* @return the type
*/
String getType();
/**
* Populate a {@link User} with backend roles. This method will not be called for cached users.
* <p/>
* Add them by calling either {@code user.addRole()} or {@code user.addRoles()}
* </P>
* @param user The authenticated user to populate with backend roles, never null
* @param credentials Credentials to authenticate to the authorization backend, maybe null.
* <em>This parameter is for future usage, currently always empty credentials are passed!</em>
* @throws ElasticsearchSecurityException in case when the authorization backend cannot be reached
* or the {@code credentials} are insufficient to authenticate to the authorization backend.
*/
void fillRoles(User user, AuthCredentials credentials) throws ElasticsearchSecurityException;
public interface AuthorizationBackend extends SyncAuthorizationBackend {
}
......@@ -3,6 +3,8 @@ package com.floragunn.searchguard.auth;
import java.util.Collections;
import java.util.List;
import com.floragunn.searchguard.auth.api.AuthorizationBackend;
public class AuthorizationDomain {
private final AuthorizationBackend authorizationBackend;
......
/*
* Copyright 2015-2020 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.
*
*/
package com.floragunn.searchguard.auth;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.floragunn.searchguard.auth.api.AuthorizationBackend;
import com.floragunn.searchguard.support.WildcardMatcher;
import com.floragunn.searchguard.user.AuthCredentials;
import com.floragunn.searchguard.user.User;
import com.google.common.cache.Cache;
public class AuthorizationProcessor {
private static final Logger log = LogManager.getLogger(AuthorizationProcessor.class);
private final Set<AuthorizationDomain> authorizationDomains;
private final Iterator<AuthorizationDomain> authorizationDomainIter;
private final Cache<User, Set<String>> roleCache;
private boolean cacheResult = true;
public AuthorizationProcessor(Set<AuthorizationDomain> authorizationDomains, Cache<User, Set<String>> roleCache) {
this.authorizationDomains = authorizationDomains;
this.authorizationDomainIter = authorizationDomains.iterator();
this.roleCache = roleCache;
}
public void authz(User authenticatedUser, Consumer<User> onSuccess, Consumer<Exception> onFailure) {
// if (authenticatedUser == null) {
// return;
// }
if (roleCache != null) {
final Set<String> cachedBackendRoles = roleCache.getIfPresent(authenticatedUser);
if (cachedBackendRoles != null) {
authenticatedUser.addRoles(new HashSet<>(cachedBackendRoles));
onSuccess.accept(authenticatedUser);
return;
}
}
if (authorizationDomains == null || authorizationDomains.isEmpty()) {
onSuccess.accept(authenticatedUser);
return;
}
checkNextAuthzDomain(authenticatedUser, onSuccess, onFailure);
}
private void checkNextAuthzDomain(User authenticatedUser, Consumer<User> onSuccess, Consumer<Exception> onFailure) {
AuthorizationDomain authorizationDomain = nextAuthorizationDomain(authenticatedUser);
if (authorizationDomain == null) {
if (roleCache != null && cacheResult) {
roleCache.put(authenticatedUser, new HashSet<>(authenticatedUser.getRoles()));
}
onSuccess.accept(authenticatedUser);
return;
}
AuthorizationBackend authorizationBackend = authorizationDomain.getAuthorizationBackend();
try {
if (log.isTraceEnabled()) {
log.trace("Backend roles for " + authenticatedUser.getName() + " not cached, return from " + authorizationBackend.getType()
+ " backend directly");
}
authorizationBackend.retrieveRoles(authenticatedUser, AuthCredentials.forUser(authenticatedUser.getName()).build(), (roles) -> {
authenticatedUser.addRoles(roles);
checkNextAuthzDomain(authenticatedUser, onSuccess, onFailure);
}, (e) -> {
log.error("Cannot retrieve roles for {} from {} due to {}", authenticatedUser, authorizationBackend.getType(), e.toString(), e);
cacheResult = false;
checkNextAuthzDomain(authenticatedUser, onSuccess, onFailure);
});
} catch (Exception e) {
log.error("Cannot retrieve roles for {} from {} due to {}", authenticatedUser, authorizationBackend.getType(), e.toString(), e);
cacheResult = false;
checkNextAuthzDomain(authenticatedUser, onSuccess, onFailure);
}
}
private AuthorizationDomain nextAuthorizationDomain(User authenticatedUser) {
while (authorizationDomainIter.hasNext()) {
AuthorizationDomain authorizationDomain = authorizationDomainIter.next();
List<String> skippedUsers = authorizationDomain.getSkippedUsers();
if (!skippedUsers.isEmpty() && authenticatedUser.getName() != null
&& WildcardMatcher.matchAny(skippedUsers, authenticatedUser.getName())) {
if (log.isDebugEnabled()) {
log.debug("Skipped authorization of user {}", authenticatedUser.getName());
}
continue;
}
return authorizationDomain;
}
return null;
}
}
/*
* Copyright 2015-2020 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.
*
*/
package com.floragunn.searchguard.auth;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.rest.RestStatus;
import com.floragunn.searchguard.auth.api.AuthenticationBackend;
import com.floragunn.searchguard.configuration.AdminDNs;
import com.floragunn.searchguard.user.User;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
public class RestImpersonationProcessor {
private static final Logger log = LogManager.getLogger(RestImpersonationProcessor.class);
private final User originalUser;
private final Collection<AuthenticationDomain> authenticationDomains;
private final Iterator<AuthenticationDomain> authenticationDomainIter;
private final Set<AuthorizationDomain> authorizationDomains;
private final AdminDNs adminDns;
private final Cache<String, User> impersonationCache;
private final String impersonatedUserHeader;
private boolean cacheResult = true;
public RestImpersonationProcessor(User originalUser, String impersonatedUserHeader, Collection<AuthenticationDomain> authenticationDomains,
Set<AuthorizationDomain> authorizationDomains, AdminDNs adminDns, Cache<String, User> impersonationCache) {
this.originalUser = originalUser;
this.authenticationDomains = authenticationDomains;
this.authenticationDomainIter = authenticationDomains.iterator();
this.authorizationDomains = authorizationDomains;
this.adminDns = adminDns;
this.impersonationCache = impersonationCache;
this.impersonatedUserHeader = impersonatedUserHeader; // restRequest.header("sg_impersonate_as");
if (Strings.isNullOrEmpty(impersonatedUserHeader) || originalUser == null) {
throw new IllegalStateException("impersonate() called with " + impersonatedUserHeader + "; " + originalUser);
}
}
public void impersonate(Consumer<AuthczResult> onResult, Consumer<Exception> onFailure) {
try {
if (adminDns.isAdminDN(impersonatedUserHeader)) {
throw new ElasticsearchSecurityException("It is not allowed to impersonate as an adminuser '" + impersonatedUserHeader + "'",
RestStatus.FORBIDDEN);
}
if (!adminDns.isRestImpersonationAllowed(originalUser.getName(), impersonatedUserHeader)) {
throw new ElasticsearchSecurityException(
"'" + originalUser.getName() + "' is not allowed to impersonate as '" + impersonatedUserHeader + "'", RestStatus.FORBIDDEN);
}
User impersonatedUser = impersonationCache.getIfPresent(impersonatedUserHeader);
if (impersonatedUser != null)