/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.aurora.scheduler.http.api.security;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.Set;
import javax.servlet.Filter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provides;
import com.google.inject.TypeLiteral;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.matcher.Matcher;
import com.google.inject.matcher.Matchers;
import com.google.inject.name.Names;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.servlet.ServletModule;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.aurora.GuiceUtils;
import org.apache.aurora.common.args.Arg;
import org.apache.aurora.common.args.CmdLine;
import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.AuroraSchedulerManager;
import org.apache.aurora.scheduler.app.MoreModules;
import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.guice.aop.ShiroAopModule;
import org.apache.shiro.guice.web.ShiroWebModule;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import static java.util.Objects.requireNonNull;
import static org.apache.aurora.scheduler.http.H2ConsoleModule.H2_PATH;
import static org.apache.aurora.scheduler.http.H2ConsoleModule.H2_PERM;
import static org.apache.aurora.scheduler.http.api.ApiModule.API_PATH;
import static org.apache.aurora.scheduler.spi.Permissions.Domain.THRIFT_AURORA_ADMIN;
import static org.apache.shiro.guice.web.ShiroWebModule.guiceFilterModule;
import static org.apache.shiro.web.filter.authc.AuthenticatingFilter.PERMISSIVE;
/**
* Provides HTTP Basic Authentication using Apache Shiro. When enabled, prevents unauthenticated
* access to write APIs and configured servlets. Write API access must also be authorized, with
* permissions configured in a shiro.ini file. For an example of this file, see the test resources
* included with this package.
*/
public class HttpSecurityModule extends ServletModule {
public static final String HTTP_REALM_NAME = "Apache Aurora Scheduler";
private static final String H2_PATTERN = H2_PATH + "/**";
private static final String ALL_PATTERN = "/**";
private static final Key<? extends Filter> K_STRICT =
Key.get(ShiroKerberosAuthenticationFilter.class);
private static final Key<? extends Filter> K_PERMISSIVE =
Key.get(ShiroKerberosPermissiveAuthenticationFilter.class);
@CmdLine(name = "shiro_realm_modules",
help = "Guice modules for configuring Shiro Realms.")
private static final Arg<Set<Module>> SHIRO_REALM_MODULE = Arg.create(
ImmutableSet.of(MoreModules.lazilyInstantiated(IniShiroRealmModule.class)));
@CmdLine(name = "shiro_after_auth_filter",
help = "Fully qualified class name of the servlet filter to be applied after the"
+ " shiro auth filters are applied.")
private static final Arg<Class<? extends Filter>> SHIRO_AFTER_AUTH_FILTER = Arg.create();
@VisibleForTesting
static final Matcher<Method> AURORA_SCHEDULER_MANAGER_SERVICE =
GuiceUtils.interfaceMatcher(AuroraSchedulerManager.Iface.class, true);
@VisibleForTesting
static final Matcher<Method> AURORA_ADMIN_SERVICE =
GuiceUtils.interfaceMatcher(AuroraAdmin.Iface.class, true);
public enum HttpAuthenticationMechanism {
/**
* No security.
*/
NONE,
/**
* HTTP Basic Authentication, produces {@link org.apache.shiro.authc.UsernamePasswordToken}s.
*/
BASIC,
/**
* Use GSS-Negotiate. Only Kerberos and SPNEGO-with-Kerberos GSS mechanisms are supported.
*/
NEGOTIATE,
}
@CmdLine(name = "http_authentication_mechanism", help = "HTTP Authentication mechanism to use.")
private static final Arg<HttpAuthenticationMechanism> HTTP_AUTHENTICATION_MECHANISM =
Arg.create(HttpAuthenticationMechanism.NONE);
private final HttpAuthenticationMechanism mechanism;
private final Set<Module> shiroConfigurationModules;
private final Optional<Key<? extends Filter>> shiroAfterAuthFilterKey;
public HttpSecurityModule() {
this(
HTTP_AUTHENTICATION_MECHANISM.get(),
SHIRO_REALM_MODULE.get(),
SHIRO_AFTER_AUTH_FILTER.hasAppliedValue() ? Key.get(SHIRO_AFTER_AUTH_FILTER.get()) : null);
}
@VisibleForTesting
HttpSecurityModule(
Module shiroConfigurationModule,
Key<? extends Filter> shiroAfterAuthFilterKey) {
this(HttpAuthenticationMechanism.BASIC,
ImmutableSet.of(shiroConfigurationModule),
shiroAfterAuthFilterKey);
}
private HttpSecurityModule(
HttpAuthenticationMechanism mechanism,
Set<Module> shiroConfigurationModules,
Key<? extends Filter> shiroAfterAuthFilterKey) {
this.mechanism = requireNonNull(mechanism);
this.shiroConfigurationModules = requireNonNull(shiroConfigurationModules);
this.shiroAfterAuthFilterKey = Optional.ofNullable(shiroAfterAuthFilterKey);
}
@Override
protected void configureServlets() {
if (mechanism == HttpAuthenticationMechanism.NONE) {
// TODO(ksweeney): Use an OptionalBinder here once we're on Guice 4.0.
bind(new TypeLiteral<Optional<Subject>>() { }).toInstance(Optional.empty());
} else {
doConfigureServlets();
}
}
private void doConfigureServlets() {
bind(Subject.class).toProvider(SecurityUtils::getSubject).in(RequestScoped.class);
install(new AbstractModule() {
@Override
protected void configure() {
// Provides-only module to provide Optional<Subject>.
// TODO(ksweeney): Use an OptionalBinder here once we're on Guice 4.0.
}
@Provides
Optional<Subject> provideOptionalSubject(Subject subject) {
return Optional.of(subject);
}
});
install(guiceFilterModule(API_PATH));
install(guiceFilterModule(H2_PATH));
install(guiceFilterModule(H2_PATH + "/*"));
install(new ShiroWebModule(getServletContext()) {
// Replace the ServletContainerSessionManager which causes subject.runAs(...) in a
// downstream user-defined filter to fail. See also: SHIRO-554
@Override
protected void bindSessionManager(AnnotatedBindingBuilder<SessionManager> bind) {
bind.to(DefaultSessionManager.class).asEagerSingleton();
}
@Override
@SuppressWarnings("unchecked")
protected void configureShiroWeb() {
for (Module module : shiroConfigurationModules) {
// We can't wrap this in a PrivateModule because Guice Multibindings don't work with them
// and we need a Set<Realm>.
install(module);
}
// Filter registration order is important here and is defined by the matching pattern:
// more specific pattern first.
switch (mechanism) {
case BASIC:
addFilterChain(H2_PATTERN, NO_SESSION_CREATION, AUTHC_BASIC, config(PERMS, H2_PERM));
addFilterChainWithAfterAuthFilter(config(AUTHC_BASIC, PERMISSIVE));
break;
case NEGOTIATE:
addFilterChain(H2_PATTERN, NO_SESSION_CREATION, K_STRICT, config(PERMS, H2_PERM));
addFilterChainWithAfterAuthFilter(K_PERMISSIVE);
break;
default:
addError("Unrecognized HTTP authentication mechanism: " + mechanism);
break;
}
}
private void addFilterChainWithAfterAuthFilter(Key<? extends Filter> filter) {
if (shiroAfterAuthFilterKey.isPresent()) {
addFilterChain(filter, shiroAfterAuthFilterKey.get());
} else {
addFilterChain(filter);
}
}
@SuppressWarnings("unchecked")
private void addFilterChain(Key<? extends Filter> filter) {
addFilterChain(ALL_PATTERN, NO_SESSION_CREATION, filter);
}
@SuppressWarnings("unchecked")
private void addFilterChain(Key<? extends Filter> filter1, Key<? extends Filter> filter2) {
addFilterChain(ALL_PATTERN, NO_SESSION_CREATION, filter1, filter2);
}
});
bindConstant().annotatedWith(Names.named("shiro.applicationName")).to(HTTP_REALM_NAME);
// TODO(ksweeney): Disable session cookie.
// TODO(ksweeney): Disable RememberMe cookie.
install(new ShiroAopModule());
// It is important that authentication happen before authorization is attempted, otherwise
// the authorizing interceptor will always fail.
MethodInterceptor authenticatingInterceptor = new ShiroAuthenticatingThriftInterceptor();
requestInjection(authenticatingInterceptor);
bindInterceptor(
Matchers.subclassesOf(AuroraSchedulerManager.Iface.class),
AURORA_SCHEDULER_MANAGER_SERVICE.or(AURORA_ADMIN_SERVICE),
authenticatingInterceptor);
MethodInterceptor apiInterceptor = new ShiroAuthorizingParamInterceptor();
requestInjection(apiInterceptor);
bindInterceptor(
Matchers.subclassesOf(AuroraSchedulerManager.Iface.class),
AURORA_SCHEDULER_MANAGER_SERVICE,
apiInterceptor);
MethodInterceptor adminInterceptor = new ShiroAuthorizingInterceptor(THRIFT_AURORA_ADMIN);
requestInjection(adminInterceptor);
bindInterceptor(
Matchers.subclassesOf(AnnotatedAuroraAdmin.class),
AURORA_ADMIN_SERVICE,
adminInterceptor);
}
}