/** * 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.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; import org.apache.aurora.gen.AuroraAdmin; import org.apache.aurora.gen.JobKey; import org.apache.aurora.gen.Response; import org.apache.aurora.gen.ResponseCode; import org.apache.aurora.scheduler.base.JobKeys; import org.apache.aurora.scheduler.http.AbstractJettyTest; import org.apache.aurora.scheduler.http.H2ConsoleModule; import org.apache.aurora.scheduler.http.api.ApiModule; import org.apache.aurora.scheduler.storage.entities.IJobKey; import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin; import org.apache.aurora.scheduler.thrift.aop.MockDecoratedThrift; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.shiro.config.Ini; import org.apache.shiro.realm.text.IniRealm; import org.apache.thrift.TException; 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; 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; public class HttpSecurityIT extends AbstractJettyTest { private static final Response OK = new Response().setResponseCode(ResponseCode.OK); private static final UsernamePasswordCredentials ROOT = new UsernamePasswordCredentials("root", "secret"); private static final UsernamePasswordCredentials WFARNER = new UsernamePasswordCredentials("wfarner", "password"); private static final UsernamePasswordCredentials UNPRIVILEGED = new UsernamePasswordCredentials("ksweeney", "12345"); private static final UsernamePasswordCredentials BACKUP_SERVICE = new UsernamePasswordCredentials("backupsvc", "s3cret!!1"); private static final UsernamePasswordCredentials DEPLOY_SERVICE = new UsernamePasswordCredentials("deploysvc", "0_0-x_0"); private static final UsernamePasswordCredentials H2_USER = new UsernamePasswordCredentials("dbuser", "pwd"); private static final UsernamePasswordCredentials INCORRECT = new UsernamePasswordCredentials("root", "wrong"); private static final UsernamePasswordCredentials NONEXISTENT = new UsernamePasswordCredentials("nobody", "12345"); private static final Set<Credentials> INVALID_CREDENTIALS = ImmutableSet.of(INCORRECT, NONEXISTENT); private static final Set<Credentials> VALID_CREDENTIALS = ImmutableSet.of(ROOT, WFARNER, UNPRIVILEGED, BACKUP_SERVICE); private static final IJobKey ADS_STAGING_JOB = JobKeys.from("ads", "staging", "job"); 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() { ini = new Ini(); Ini.Section users = ini.addSection(IniRealm.USERS_SECTION_NAME); users.put(ROOT.getUserName(), COMMA_JOINER.join(ROOT.getPassword(), ADMIN_ROLE)); users.put(WFARNER.getUserName(), COMMA_JOINER.join(WFARNER.getPassword(), ENG_ROLE)); users.put(UNPRIVILEGED.getUserName(), UNPRIVILEGED.getPassword()); users.put( BACKUP_SERVICE.getUserName(), COMMA_JOINER.join(BACKUP_SERVICE.getPassword(), BACKUP_ROLE)); users.put( DEPLOY_SERVICE.getUserName(), COMMA_JOINER.join(DEPLOY_SERVICE.getPassword(), DEPLOY_ROLE)); users.put(H2_USER.getUserName(), COMMA_JOINER.join(H2_USER.getPassword(), H2_ROLE)); Ini.Section roles = ini.addSection(IniRealm.ROLES_SECTION_NAME); roles.put(ADMIN_ROLE, "*"); roles.put(ENG_ROLE, "thrift.AuroraSchedulerManager:*"); roles.put(BACKUP_ROLE, "thrift.AuroraAdmin:listBackups"); roles.put( DEPLOY_ROLE, "thrift.AuroraSchedulerManager:killTasks:" + ADS_STAGING_JOB.getRole() + ":" + ADS_STAGING_JOB.getEnvironment() + ":" + ADS_STAGING_JOB.getName()); roles.put(H2_ROLE, H2_PERM); auroraAdmin = createMock(AnnotatedAuroraAdmin.class); shiroAfterAuthFilter = createMock(Filter.class); } @Override protected Module getChildServletModule() { return Modules.combine( new ApiModule(), new H2ConsoleModule(true), 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); } }); } private AuroraAdmin.Client getUnauthenticatedClient() throws TTransportException { return getClient(null); } private String formatUrl(String endpoint) { return "http://" + httpServer.getHostText() + ":" + httpServer.getPort() + endpoint; } private AuroraAdmin.Client getClient(HttpClient httpClient) throws TTransportException { final TTransport httpClientTransport = new THttpClient(formatUrl(API_PATH), httpClient); addTearDown(httpClientTransport::close); return new AuroraAdmin.Client(new TJSONProtocol(httpClientTransport)); } private AuroraAdmin.Client getAuthenticatedClient(Credentials credentials) throws TTransportException { DefaultHttpClient defaultHttpClient = new DefaultHttpClient(); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, credentials); defaultHttpClient.setCredentialsProvider(credentialsProvider); 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, ServletException, IOException { expect(auroraAdmin.getRoleSummary()).andReturn(OK).times(3); expectShiroAfterAuthFilter().times(3); replayAndStart(); assertEquals(OK, getUnauthenticatedClient().getRoleSummary()); assertEquals(OK, getAuthenticatedClient(ROOT).getRoleSummary()); // Incorrect works because the server doesn't challenge for credentials to execute read-only // methods. assertEquals(OK, getAuthenticatedClient(INCORRECT).getRoleSummary()); } private void assertKillTasksFails(AuroraAdmin.Client client) throws TException { try { client.killTasks(null, null, null); fail("killTasks should fail."); } catch (TTransportException e) { // Expected. } } @Test public void testAuroraSchedulerManager() throws TException, ServletException, IOException { JobKey job = JobKeys.from("role", "env", "name").newBuilder(); expect(auroraAdmin.killTasks(job, null, null)).andReturn(OK).times(2); expect(auroraAdmin.killTasks(ADS_STAGING_JOB.newBuilder(), null, null)).andReturn(OK); expectShiroAfterAuthFilter().atLeastOnce(); replayAndStart(); assertEquals( OK, getAuthenticatedClient(WFARNER).killTasks(job, null, null)); assertEquals( OK, getAuthenticatedClient(ROOT).killTasks(job, null, null)); assertEquals( ResponseCode.INVALID_REQUEST, getAuthenticatedClient(UNPRIVILEGED).killTasks(null, null, null).getResponseCode()); assertEquals( ResponseCode.AUTH_FAILED, getAuthenticatedClient(UNPRIVILEGED) .killTasks(job, null, null) .getResponseCode()); assertEquals( ResponseCode.INVALID_REQUEST, getAuthenticatedClient(BACKUP_SERVICE).killTasks(null, null, null).getResponseCode()); assertEquals( ResponseCode.AUTH_FAILED, getAuthenticatedClient(BACKUP_SERVICE) .killTasks(job, null, null) .getResponseCode()); assertEquals( ResponseCode.AUTH_FAILED, getAuthenticatedClient(DEPLOY_SERVICE) .killTasks(job, null, null) .getResponseCode()); assertEquals( OK, getAuthenticatedClient(DEPLOY_SERVICE).killTasks( ADS_STAGING_JOB.newBuilder(), null, null)); assertKillTasksFails(getUnauthenticatedClient()); assertKillTasksFails(getAuthenticatedClient(INCORRECT)); assertKillTasksFails(getAuthenticatedClient(NONEXISTENT)); } private void assertSnapshotFails(AuroraAdmin.Client client) throws TException { try { client.snapshot(); fail("snapshot should fail"); } catch (TTransportException e) { // Expected. } } @Test public void testAuroraAdmin() throws TException, ServletException, IOException { expect(auroraAdmin.snapshot()).andReturn(OK); expect(auroraAdmin.listBackups()).andReturn(OK); expectShiroAfterAuthFilter().times(12); replayAndStart(); assertEquals(OK, getAuthenticatedClient(ROOT).snapshot()); for (Credentials credentials : INVALID_CREDENTIALS) { assertSnapshotFails(getAuthenticatedClient(credentials)); } for (Credentials credentials : Sets.difference(VALID_CREDENTIALS, ImmutableSet.of(ROOT))) { assertEquals( ResponseCode.AUTH_FAILED, getAuthenticatedClient(credentials).snapshot().getResponseCode()); } assertEquals(OK, getAuthenticatedClient(BACKUP_SERVICE).listBackups()); } private HttpResponse callH2Console(Credentials credentials) throws Exception { DefaultHttpClient defaultHttpClient = new DefaultHttpClient(); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, credentials); defaultHttpClient.setCredentialsProvider(credentialsProvider); return defaultHttpClient.execute(new HttpPost(formatUrl(H2_PATH + "/"))); } @Test public void testH2ConsoleUser() throws Exception { replayAndStart(); assertEquals( ClientResponse.Status.OK.getStatusCode(), callH2Console(H2_USER).getStatusLine().getStatusCode()); } @Test public void testH2ConsoleAdmin() throws Exception { replayAndStart(); assertEquals( ClientResponse.Status.OK.getStatusCode(), callH2Console(ROOT).getStatusLine().getStatusCode()); } @Test public void testH2ConsoleUnauthorized() throws Exception { replayAndStart(); assertEquals( ClientResponse.Status.UNAUTHORIZED.getStatusCode(), callH2Console(UNPRIVILEGED).getStatusLine().getStatusCode()); } }