/*
* JBoss, Home of Professional Open Source.
* Copyright 2017, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.wildfly.test.integration.elytron.batch;
import static org.jboss.as.controller.client.helpers.ClientConstants.ADDRESS;
import static org.jboss.as.controller.client.helpers.ClientConstants.OP;
import static org.jboss.as.controller.client.helpers.ClientConstants.UNDEFINE_ATTRIBUTE_OPERATION;
import static org.jboss.as.controller.client.helpers.ClientConstants.WRITE_ATTRIBUTE_OPERATION;
import java.security.AllPermission;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.batch.operations.JobOperator;
import javax.batch.operations.JobSecurityException;
import javax.batch.runtime.BatchRuntime;
import javax.batch.runtime.BatchStatus;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.as.arquillian.api.ServerSetup;
import org.jboss.as.arquillian.api.ServerSetupTask;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.test.shared.ServerReload;
import org.jboss.as.test.shared.TimeoutUtil;
import org.jboss.as.test.shared.integration.ejb.security.PermissionUtils;
import org.jboss.dmr.ModelNode;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.extension.batch.jberet.deployment.BatchPermission;
import org.wildfly.security.auth.permission.LoginPermission;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import org.wildfly.test.security.common.AbstractElytronSetupTask;
import org.wildfly.test.security.common.elytron.ConfigurableElement;
import org.wildfly.test.security.common.elytron.EJBApplicationSecurityDomainMapping;
import org.wildfly.test.security.common.elytron.PermissionRef;
import org.wildfly.test.security.common.elytron.PropertyFileBasedDomain;
import org.wildfly.test.security.common.elytron.SimplePermissionMapper;
/**
* This is for testing the BatchPermission from batch-jberet subsystem.
* It also checks that when running a Batch job as a particular user, the security identity can be retrieved
* within the job's code.
*
* The security setup is like this:
* user1/password1 -> can do everything
* user2/password2 -> can stop jobs only
* user3/password3 -> can read jobs only
*
* @author Jan Martiska
*/
@ServerSetup({BatchSubsystemSecurityTestCase.CreateBatchSecurityDomainSetupTask.class,
BatchSubsystemSecurityTestCase.ActivateBatchSecurityDomainSetupTask.class})
@RunWith(Arquillian.class)
public class BatchSubsystemSecurityTestCase {
static final String BATCH_SECURITY_DOMAIN_NAME = "BatchDomain";
@Deployment
public static Archive<?> createDeployment() {
final JavaArchive jar = ShrinkWrap.create(JavaArchive.class, "batch-test.jar");
jar.addClasses(AbstractElytronSetupTask.class,
SecurityDomainSettingEJB.class,
TimeoutUtil.class,
IdentityBatchlet.class,
FailingBatchlet.class,
LongRunningBatchlet.class);
jar.addAsManifestResource(BatchSubsystemSecurityTestCase.class.getPackage(),
"assert-identity.xml",
"batch-jobs/assert-identity.xml");
jar.addAsManifestResource(BatchSubsystemSecurityTestCase.class.getPackage(),
"failing-batchlet.xml",
"batch-jobs/failing-batchlet.xml");
jar.addAsManifestResource(BatchSubsystemSecurityTestCase.class.getPackage(),
"long-running-batchlet.xml",
"batch-jobs/long-running-batchlet.xml");
jar.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml");
jar.addAsManifestResource(PermissionUtils.createPermissionsXmlAsset(new AllPermission()), "permissions.xml");
return jar;
}
// this is the identityWithinJob using which the batch job was invoked
// the job itself completes this future when it runs
static volatile CompletableFuture<String> identityWithinJob;
private JobOperator operator;
@Before
public void before() {
operator = BatchRuntime.getJobOperator();
}
/**
* Try running a job as a user who has the permission to run jobs. It should succeed.
* The job should also be able to retrieve the name of the user who ran it.
*/
@Test
public void testStart_Allowed() throws Exception {
identityWithinJob = new CompletableFuture<>();
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
user1.runAs((Callable<Long>)() -> operator.start("assert-identity", new Properties()));
final String actualUsername = identityWithinJob.get(TimeoutUtil.adjust(20), TimeUnit.SECONDS);
Assert.assertEquals("user1", actualUsername);
}
/**
* Try running a job as a user who doesn't have the permission to run jobs. It should not succeed.
*/
@Test
public void testStart_NotAllowed() throws Exception {
final SecurityIdentity user2 = getSecurityIdentity("user2", "password2");
try {
user2.runAs((Callable<Long>)() -> operator.start("assert-identity", new Properties()));
Assert.fail("user2 shouldn't be allowed to start batch jobs");
} catch(JobSecurityException e) {
// OK
}
}
/**
* Test reading execution metadata by a user who has the permission to do it.
* User1 runs a job and then user2 tries to read its metadata.
*/
@Test
public void testRead_Allowed() throws Exception {
final Properties jobParams = new Properties();
jobParams.put("prop1", "val1");
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final SecurityIdentity user3 = getSecurityIdentity("user3", "password3");
final Long executionId = user1
.runAs((Callable<Long>)() -> operator.start("assert-identity", jobParams));
final Properties retrievedParams = user3
.runAs((Callable<Properties>)() -> operator.getJobExecution(executionId).getJobParameters());
Assert.assertEquals(jobParams, retrievedParams);
}
/**
* Test reading execution metadata by a user who doesn't have the permission to do it.
* User1 runs a job and then user2 tries to read its metadata.
*/
@Test
public void testRead_NotAllowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final SecurityIdentity user2 = getSecurityIdentity("user2", "password2");
final Long executionId = user1
.runAs((Callable<Long>)() -> operator.start("assert-identity", new Properties()));
try {
user2.runAs((Callable<Properties>)() -> operator.getJobExecution(executionId).getJobParameters());
Assert.fail("user2 shouldn't be allowed to read batch job metadata");
} catch(JobSecurityException e) {
// OK
}
}
/**
* Test restarting failed jobs by a user who has the permission to do it.
*/
@Test
public void testRestart_Allowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
Properties params = new Properties();
params.put("should.fail", "true");
final Long executionId = user1
.runAs((Callable<Long>)() -> operator.start("failing-batchlet", params));
waitForJobEnd(executionId, 10);
Assert.assertEquals(BatchStatus.FAILED, operator.getJobExecution(executionId).getBatchStatus());
params.put("should.fail", "false");
final Long executionIdAfterRestart = user1
.runAs((Callable<Long>)() -> operator.restart(executionId, params));
waitForJobEnd(executionIdAfterRestart, 10);
Assert.assertEquals(BatchStatus.COMPLETED, operator.getJobExecution(executionIdAfterRestart).getBatchStatus());
}
/**
* Test restarting failed jobs by a user who doesn't have the permission to do it.
*/
@Test
public void testRestart_NotAllowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final SecurityIdentity user2 = getSecurityIdentity("user2", "password2");
Properties params = new Properties();
params.put("should.fail", "true");
final Long executionId = user1
.runAs((Callable<Long>)() -> operator.start("failing-batchlet", params));
waitForJobEnd(executionId, 10);
Assert.assertEquals(BatchStatus.FAILED, operator.getJobExecution(executionId).getBatchStatus());
try {
user2.runAs((Callable<Long>)() -> operator.restart(executionId, params));
Assert.fail("user2 shouldn't be allowed to restart batch jobs");
} catch(JobSecurityException e) {
// OK
}
}
/**
* Abandoning an execution by a user who has the permission to do it.
*/
@Test
public void testAbandon_Allowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final Long id = user1.runAs((Callable<Long>)() -> operator.start("assert-identity", new Properties()));
waitForJobEnd(id, 10);
user1.runAs(() -> operator.abandon(id));
Assert.assertEquals(operator.getJobExecution(id).getBatchStatus(), BatchStatus.ABANDONED);
}
/**
* Abandoning an execution by a user who doesn't have the permission to do it.
*/
@Test
public void testAbandon_NotAllowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final SecurityIdentity user2 = getSecurityIdentity("user2", "password2");
final Long id = user1.runAs((Callable<Long>)() -> operator.start("assert-identity", new Properties()));
waitForJobEnd(id, 10);
try {
user2.runAs(() -> operator.abandon(id));
Assert.fail("user2 should not be allowed to abandon job executions");
} catch(JobSecurityException e) {
// OK
}
Assert.assertEquals(operator.getJobExecution(id).getBatchStatus(), BatchStatus.COMPLETED);
}
/**
* Stopping an execution by a user who doesn't have the permission to do it.
*/
@Test
public void testStop_NotAllowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final SecurityIdentity user3 = getSecurityIdentity("user3", "password3");
final Long id = user1.runAs((Callable<Long>)() -> operator.start("long-running-batchlet", null));
TimeUnit.SECONDS.sleep(1);
try {
user3.runAs(() -> operator.stop(id));
Assert.fail("user2 should not be allowed to stop job executions");
} catch(JobSecurityException e) {
// OK
}
Assert.assertNotEquals(BatchStatus.STOPPED, operator.getJobExecution(id).getBatchStatus());
}
/**
* Stopping an execution by a user who has the permission to do it.
*/
@Test
public void testStop_Allowed() throws Exception {
final SecurityIdentity user1 = getSecurityIdentity("user1", "password1");
final Long id = user1.runAs((Callable<Long>)() -> operator.start("long-running-batchlet", null));
TimeUnit.SECONDS.sleep(1);
user1.runAs(() -> operator.stop(id));
waitForJobEnd(id, 10);
Assert.assertEquals(BatchStatus.STOPPED, operator.getJobExecution(id).getBatchStatus());
}
private void waitForJobEnd(Long id, int timeoutSeconds) throws TimeoutException {
Long start = System.currentTimeMillis();
final JobOperator operator = BatchRuntime.getJobOperator();
while(System.currentTimeMillis() - start < (TimeoutUtil.adjust(timeoutSeconds) * 1000)) {
if(operator.getJobExecution(id).getEndTime() != null)
return;
}
throw new TimeoutException();
}
private static SecurityIdentity getSecurityIdentity(String username, String password)
throws RealmUnavailableException {
return SecurityDomain.getCurrent().authenticate(username, new PasswordGuessEvidence(password.toCharArray()));
}
static class CreateBatchSecurityDomainSetupTask extends AbstractElytronSetupTask {
@Override
protected ConfigurableElement[] getConfigurableElements() {
return new ConfigurableElement[] {
SimplePermissionMapper.builder().withName("batch-permission-mapper")
.permissionMappings(
SimplePermissionMapper.PermissionMapping.builder()
.withPrincipals("user1", "anonymous")
.withPermissions(
PermissionRef.builder()
.targetName("*")
.className(BatchPermission.class.getName())
.module("org.wildfly.extension.batch.jberet")
.build(),
PermissionRef.builder()
.className(LoginPermission.class.getName())
.build())
.build(),
SimplePermissionMapper.PermissionMapping.builder()
.withPrincipals("user2")
.withPermissions(
PermissionRef.builder()
.targetName("stop")
.className(BatchPermission.class.getName())
.module("org.wildfly.extension.batch.jberet")
.build(),
PermissionRef.builder()
.className(LoginPermission.class.getName())
.build())
.build(),
SimplePermissionMapper.PermissionMapping.builder()
.withPrincipals("user3")
.withPermissions(
PermissionRef.builder()
.targetName("read")
.className(BatchPermission.class.getName())
.module("org.wildfly.extension.batch.jberet")
.build(),
PermissionRef.builder()
.className(LoginPermission.class.getName())
.build())
.build()
).build(),
PropertyFileBasedDomain.builder().withName(BATCH_SECURITY_DOMAIN_NAME)
.permissionMapper("batch-permission-mapper")
.withUser("user1", "password1")
.withUser("user2", "password2")
.withUser("user3", "password3")
.build(),
new EJBApplicationSecurityDomainMapping(BATCH_SECURITY_DOMAIN_NAME, BATCH_SECURITY_DOMAIN_NAME)
};
}
}
static class ActivateBatchSecurityDomainSetupTask implements ServerSetupTask {
final ModelNode BATCH_SUBSYSTEM_ADDRESS = PathAddress.pathAddress("subsystem", "batch-jberet")
.toModelNode();
@Override
public void setup(ManagementClient managementClient, String s) throws Exception {
final ModelNode setOp = new ModelNode();
setOp.get(OP).set(WRITE_ATTRIBUTE_OPERATION);
setOp.get(ADDRESS).set(BATCH_SUBSYSTEM_ADDRESS);
setOp.get("name").set("security-domain");
setOp.get("value").set(BATCH_SECURITY_DOMAIN_NAME);
final ModelNode result = managementClient.getControllerClient().execute(setOp);
Assert.assertTrue(result.get("outcome").asString().equals("success"));
ServerReload.executeReloadAndWaitForCompletion(managementClient.getControllerClient());
}
@Override
public void tearDown(ManagementClient managementClient, String s) throws Exception {
final ModelNode undefineOp = new ModelNode();
undefineOp.get(OP).set(UNDEFINE_ATTRIBUTE_OPERATION);
undefineOp.get(ADDRESS).set(BATCH_SUBSYSTEM_ADDRESS);
undefineOp.get("name").set("security-domain");
final ModelNode result = managementClient.getControllerClient().execute(undefineOp);
Assert.assertTrue(result.get("outcome").asString().equals("success"));
ServerReload.executeReloadAndWaitForCompletion(managementClient.getControllerClient());
}
}
}