Repository: aurora Updated Branches: refs/heads/master e0fef5abf -> 4dff5da84
Allow for plugging in cli-configurable filters that are invoked post shiro filters. Bugs closed: AURORA-1576 Reviewed at https://reviews.apache.org/r/42046/ Project: http://git-wip-us.apache.org/repos/asf/aurora/repo Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/4dff5da8 Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/4dff5da8 Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/4dff5da8 Branch: refs/heads/master Commit: 4dff5da84e078aa8ab85be01d7c71495e2f1a940 Parents: e0fef5a Author: Amol Deshmukh <[email protected]> Authored: Thu Jan 14 16:26:46 2016 -0600 Committer: Joshua Cohen <[email protected]> Committed: Thu Jan 14 16:26:46 2016 -0600 ---------------------------------------------------------------------- NEWS | 3 + docs/security.md | 8 +++ .../http/api/security/HttpSecurityModule.java | 58 +++++++++++++++--- .../http/api/security/HttpSecurityIT.java | 63 +++++++++++++++----- 4 files changed, 111 insertions(+), 21 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/aurora/blob/4dff5da8/NEWS ---------------------------------------------------------------------- diff --git a/NEWS b/NEWS index 809077f..3937108 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,9 @@ `-enable_cors_for`. - `-deduplicate_snapshots` and `-deflate_snapshots`. These features are good to always enable. - `-enable_job_updates` and `-enable_job_creation` +- Added scheduler command line argument: + - `-shiro_after_auth_filter`. Optionally specify a class implementing javax.servlet.Filter + that will be included in the Filter chain following the Shiro auth filters. - Upgraded the scheduler ZooKeeper client from 3.3.4 to 3.4.6. - Added support for jobs to specify arbitrary ZooKeeper paths for service registration. See https://github.com/apache/aurora/blob/master/docs/configuration-reference.md#announcer-objects http://git-wip-us.apache.org/repos/asf/aurora/blob/4dff5da8/docs/security.md ---------------------------------------------------------------------- diff --git a/docs/security.md b/docs/security.md index f9b60e9..32bea42 100644 --- a/docs/security.md +++ b/docs/security.md @@ -217,6 +217,14 @@ You might find documentation on the Internet suggesting there are additional sec like `[main]` and `[urls]`. These are not supported by Aurora as it uses a different mechanism to configure those parts of Shiro. Think of Aurora's `security.ini` as a subset with only `[users]` and `[roles]` sections. +## Implementing Delegated Authorization + +It is possible to leverage Shiro's `runAs` feature by implementing a custom Servlet Filter that provides +the capability and passing it's fully qualified class name to the command line argument +`-shiro_after_auth_filter`. The filter is registered in the same filter chain as the Shiro auth filters +and is placed after the Shiro auth filters in the filter chain. This ensures that the Filter is invoked +after the Shiro filters have had a chance to authenticate the request. + # Implementing a Custom Realm Since Auroraâs security is backed by [Apache Shiro](https://shiro.apache.org), you can implement a http://git-wip-us.apache.org/repos/asf/aurora/blob/4dff5da8/src/main/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityModule.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityModule.java index 4b6a872..e328620 100644 --- a/src/main/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityModule.java +++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityModule.java @@ -16,7 +16,6 @@ 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; @@ -26,6 +25,7 @@ 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; @@ -43,6 +43,8 @@ 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; @@ -76,6 +78,11 @@ public class HttpSecurityModule extends ServletModule { 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); @@ -107,22 +114,33 @@ public class HttpSecurityModule extends ServletModule { 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()); + 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) { - this(HttpAuthenticationMechanism.BASIC, ImmutableSet.of(shiroConfigurationModule)); + HttpSecurityModule( + Module shiroConfigurationModule, + Key<? extends Filter> shiroAfterAuthFilterKey) { + + this(HttpAuthenticationMechanism.BASIC, + ImmutableSet.of(shiroConfigurationModule), + shiroAfterAuthFilterKey); } private HttpSecurityModule( HttpAuthenticationMechanism mechanism, - Set<Module> shiroConfigurationModules) { + Set<Module> shiroConfigurationModules, + Key<? extends Filter> shiroAfterAuthFilterKey) { this.mechanism = requireNonNull(mechanism); this.shiroConfigurationModules = requireNonNull(shiroConfigurationModules); + this.shiroAfterAuthFilterKey = Optional.ofNullable(shiroAfterAuthFilterKey); } @Override @@ -153,6 +171,14 @@ public class HttpSecurityModule extends ServletModule { 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() { @@ -167,12 +193,12 @@ public class HttpSecurityModule extends ServletModule { switch (mechanism) { case BASIC: addFilterChain(H2_PATTERN, NO_SESSION_CREATION, AUTHC_BASIC, config(PERMS, H2_PERM)); - addFilterChain(ALL_PATTERN, NO_SESSION_CREATION, config(AUTHC_BASIC, PERMISSIVE)); + addFilterChainWithAfterAuthFilter(config(AUTHC_BASIC, PERMISSIVE)); break; case NEGOTIATE: addFilterChain(H2_PATTERN, NO_SESSION_CREATION, K_STRICT, config(PERMS, H2_PERM)); - addFilterChain(ALL_PATTERN, NO_SESSION_CREATION, K_PERMISSIVE); + addFilterChainWithAfterAuthFilter(K_PERMISSIVE); break; default: @@ -180,6 +206,24 @@ public class HttpSecurityModule extends ServletModule { 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); http://git-wip-us.apache.org/repos/asf/aurora/blob/4dff5da8/src/test/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityIT.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityIT.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityIT.java index ac92117..3e811a4 100644 --- a/src/test/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityIT.java +++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/HttpSecurityIT.java @@ -15,12 +15,20 @@ package org.apache.aurora.scheduler.http.api.security; import java.io.IOException; import java.util.Set; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.inject.AbstractModule; +import com.google.inject.Key; import com.google.inject.Module; +import com.google.inject.name.Named; +import com.google.inject.name.Names; import com.google.inject.util.Modules; import com.sun.jersey.api.client.ClientResponse; @@ -53,6 +61,7 @@ import org.apache.thrift.protocol.TJSONProtocol; import org.apache.thrift.transport.THttpClient; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; +import org.easymock.IExpectationSetters; import org.junit.Before; import org.junit.Test; @@ -60,6 +69,9 @@ 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.easymock.EasyMock.expect; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.getCurrentArguments; +import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; @@ -92,15 +104,17 @@ public class HttpSecurityIT extends AbstractJettyTest { private static final IJobKey ADS_STAGING_JOB = JobKeys.from("ads", "staging", "job"); - private Ini ini; - private AnnotatedAuroraAdmin auroraAdmin; - private static final Joiner COMMA_JOINER = Joiner.on(", "); private static final String ADMIN_ROLE = "admin"; private static final String ENG_ROLE = "eng"; private static final String BACKUP_ROLE = "backup"; private static final String DEPLOY_ROLE = "deploy"; private static final String H2_ROLE = "h2access"; + private static final Named SHIRO_AFTER_AUTH_FILTER_ANNOTATION = Names.named("shiro_post_filter"); + + private Ini ini; + private AnnotatedAuroraAdmin auroraAdmin; + private Filter shiroAfterAuthFilter; @Before public void setUp() { @@ -133,6 +147,7 @@ public class HttpSecurityIT extends AbstractJettyTest { roles.put(H2_ROLE, H2_PERM); auroraAdmin = createMock(AnnotatedAuroraAdmin.class); + shiroAfterAuthFilter = createMock(Filter.class); } @Override @@ -140,10 +155,15 @@ public class HttpSecurityIT extends AbstractJettyTest { return Modules.combine( new ApiModule(), new H2ConsoleModule(true), - new HttpSecurityModule(new IniShiroRealmModule(ini)), + new HttpSecurityModule( + new IniShiroRealmModule(ini), + Key.get(Filter.class, SHIRO_AFTER_AUTH_FILTER_ANNOTATION)), new AbstractModule() { @Override protected void configure() { + bind(Filter.class) + .annotatedWith(SHIRO_AFTER_AUTH_FILTER_ANNOTATION) + .toInstance(shiroAfterAuthFilter); MockDecoratedThrift.bindForwardedMock(binder(), auroraAdmin); } }); @@ -175,9 +195,25 @@ public class HttpSecurityIT extends AbstractJettyTest { return getClient(defaultHttpClient); } + private IExpectationSetters<Object> expectShiroAfterAuthFilter() + throws ServletException, IOException { + + shiroAfterAuthFilter.doFilter( + isA(HttpServletRequest.class), + isA(HttpServletResponse.class), + isA(FilterChain.class)); + + return expectLastCall().andAnswer(() -> { + Object[] args = getCurrentArguments(); + ((FilterChain) args[2]).doFilter((HttpServletRequest) args[0], (HttpServletResponse) args[1]); + return null; + }); + } + @Test - public void testReadOnlyScheduler() throws TException { + public void testReadOnlyScheduler() throws TException, ServletException, IOException { expect(auroraAdmin.getRoleSummary()).andReturn(OK).times(3); + expectShiroAfterAuthFilter().times(3); replayAndStart(); @@ -198,7 +234,7 @@ public class HttpSecurityIT extends AbstractJettyTest { } @Test - public void testAuroraSchedulerManager() throws TException, IOException { + public void testAuroraSchedulerManager() throws TException, ServletException, IOException { expect(auroraAdmin.killTasks(null, new Lock().setMessage("1"))).andReturn(OK); expect(auroraAdmin.killTasks(null, new Lock().setMessage("2"))).andReturn(OK); @@ -206,12 +242,12 @@ public class HttpSecurityIT extends AbstractJettyTest { TaskQuery adsScopedQuery = Query.jobScoped(ADS_STAGING_JOB).get(); expect(auroraAdmin.killTasks(adsScopedQuery, null)).andReturn(OK); + expectShiroAfterAuthFilter().times(19); + replayAndStart(); - assertEquals(OK, - getAuthenticatedClient(WFARNER).killTasks(null, new Lock().setMessage("1"))); - assertEquals(OK, - getAuthenticatedClient(ROOT).killTasks(null, new Lock().setMessage("2"))); + assertEquals(OK, getAuthenticatedClient(WFARNER).killTasks(null, new Lock().setMessage("1"))); + assertEquals(OK, getAuthenticatedClient(ROOT).killTasks(null, new Lock().setMessage("2"))); assertEquals( ResponseCode.INVALID_REQUEST, getAuthenticatedClient(UNPRIVILEGED).killTasks(null, null).getResponseCode()); @@ -233,9 +269,7 @@ public class HttpSecurityIT extends AbstractJettyTest { getAuthenticatedClient(DEPLOY_SERVICE) .killTasks(jobScopedQuery, null) .getResponseCode()); - assertEquals( - OK, - getAuthenticatedClient(DEPLOY_SERVICE).killTasks(adsScopedQuery, null)); + assertEquals(OK, getAuthenticatedClient(DEPLOY_SERVICE).killTasks(adsScopedQuery, null)); assertKillTasksFails(getUnauthenticatedClient()); assertKillTasksFails(getAuthenticatedClient(INCORRECT)); @@ -252,9 +286,10 @@ public class HttpSecurityIT extends AbstractJettyTest { } @Test - public void testAuroraAdmin() throws TException { + public void testAuroraAdmin() throws TException, ServletException, IOException { expect(auroraAdmin.snapshot()).andReturn(OK); expect(auroraAdmin.listBackups()).andReturn(OK); + expectShiroAfterAuthFilter().times(12); replayAndStart();
