/*
* JBoss, Home of Professional Open Source.
* Copyright 2013, 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.jboss.as.test.integration.ejb.container.interceptor.security;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.ejb.EJBAccessException;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
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.test.integration.security.common.AbstractSecurityDomainsServerSetupTask;
import org.jboss.as.test.integration.security.common.AbstractSecurityRealmsServerSetupTask;
import org.jboss.as.test.integration.security.common.Utils;
import org.jboss.as.test.integration.security.common.config.SecurityDomain;
import org.jboss.as.test.integration.security.common.config.SecurityModule;
import org.jboss.as.test.integration.security.common.config.realm.SecurityRealm;
import org.jboss.as.test.integration.security.common.config.realm.ServerIdentity;
import org.jboss.as.test.shared.util.AssumeTestGroupUtil;
import org.jboss.ejb.client.EJBClientContext;
import org.jboss.resteasy.plugins.server.embedded.SimplePrincipal;
import org.jboss.security.SecurityContextAssociation;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.wildfly.security.auth.client.AuthenticationConfiguration;
import org.wildfly.security.auth.client.AuthenticationContext;
import org.wildfly.security.auth.client.MatchRule;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Property;
import org.xnio.Sequence;
/**
* Testcase based on ejb-security-interceptors quickstart application. It tests security context propagation for EJBs.
*
* @author Josef Cacek
* @author <a href="mailto:tadamski@redhat.com">Tomasz Adamski</a>
*/
@RunWith(Arquillian.class)
@ServerSetup({ SwitchIdentityTestCase.SecurityDomainsSetup.class, //
SwitchIdentityTestCase.SecurityRealmsSetup.class })
@RunAsClient
public class SwitchIdentityTestCase {
private static final String EJB_OUTBOUND_REALM = "ejb-outbound-realm";
private static final String SECURITY_DOMAIN_NAME = "switch-identity-test";
@ArquillianResource
private ManagementClient mgmtClient;
private final Map<String, String> passwordsToUse;
public SwitchIdentityTestCase() {
passwordsToUse = new HashMap<>();
passwordsToUse.put("guest", "guest");
passwordsToUse.put("user1", "password1");
passwordsToUse.put("user2", "password2");
passwordsToUse.put("remoteejbuser", "rem@teejbpasswd1");
}
// Public methods --------------------------------------------------------
@BeforeClass
public static void beforeClass() {
AssumeTestGroupUtil.assumeElytronProfileTestsEnabled();
}
/**
* Creates a deployment application for this test.
*
* @return
* @throws IOException
*/
@Deployment
public static JavaArchive createDeployment() throws IOException {
final JavaArchive jar = ShrinkWrap.create(JavaArchive.class, EJBUtil.APPLICATION_NAME + ".jar");
jar.addClasses(GuestDelegationLoginModule.class, EJBUtil.class, Manage.class, BridgeBean.class, TargetBean.class,
CurrentUserCredential.class, ServerSecurityInterceptor.class, ClientSecurityInterceptor.class);
jar.addAsManifestResource(SwitchIdentityTestCase.class.getPackage(), "jboss-ejb3.xml", "jboss-ejb3.xml");
jar.addAsManifestResource(new StringAsset(ClientSecurityInterceptor.class.getName()),
"services/org.jboss.ejb.client.EJBClientInterceptor");
jar.addAsManifestResource(org.jboss.as.test.integration.ejb.container.interceptor.security.api.ClientSecurityInterceptor.class.getPackage(), "permissions.xml", "permissions.xml");
jar.addAsManifestResource(Utils.getJBossDeploymentStructure("org.jboss.remoting3", "org.jboss.as.domain-management",
"org.jboss.as.controller", "org.jboss.as.core-security"), "jboss-deployment-structure.xml");
return jar;
}
/**
* Test identity propagation using SecurityContextAssociation API from the client.
*
* @throws Exception
*/
@Test
public void testSecurityContextAssociation() throws Exception {
callUsingSecurityContextAssociation("guest", false, false);
callUsingSecurityContextAssociation("user1", true, false);
callUsingSecurityContextAssociation("user2", false, true);
}
/**
* Test identity propagation using LoginContext API from the client.
*
* @throws Exception
*/
@Test
public void testClientLoginModule() throws Exception {
callUsingClientLoginModule("guest", false, false);
callUsingClientLoginModule("user1", true, false);
callUsingClientLoginModule("user2", false, true);
}
// Private methods -------------------------------------------------------
/**
* Perform the tests using the ClientLoginModule and LoginContext API to set the desired Principal.
*/
private void callUsingClientLoginModule(String userName, boolean hasRole1, boolean hasRole2) throws Exception {
AuthenticationContext authenticationContext = setupAuthenticationContext(userName);
authenticationContext.runCallable(() -> {
// register the client side interceptor
final EJBClientContext ejbClientContext = EJBClientContext.getCurrent().withAddedInterceptors(new ClientSecurityInterceptor());
ejbClientContext.runCallable(() -> {
final Manage targetBean = EJBUtil.lookupEJB(TargetBean.class, Manage.class);
final Manage bridgeBean = EJBUtil.lookupEJB(BridgeBean.class, Manage.class);
//test direct access
testMethodAccess(targetBean, ManageMethodEnum.ALLROLES, true);
testMethodAccess(targetBean, ManageMethodEnum.ROLE1, hasRole1);
testMethodAccess(targetBean, ManageMethodEnum.ROLE2, hasRole2);
//test security context propagation
testMethodAccess(bridgeBean, ManageMethodEnum.ALLROLES, true);
testMethodAccess(bridgeBean, ManageMethodEnum.ROLE1, hasRole1);
testMethodAccess(bridgeBean, ManageMethodEnum.ROLE2, hasRole2);
return null;
});
return null;
});
}
private AuthenticationContext setupAuthenticationContext(final String username) {
OptionMap.Builder builder = OptionMap.builder().set(Options.SASL_POLICY_NOANONYMOUS, true);
builder.set(Options.SASL_POLICY_NOPLAINTEXT, false);
builder.set(Options.SASL_DISALLOWED_MECHANISMS, Sequence.of("JBOSS-LOCAL-USER"));
final AuthenticationContext authenticationContext = AuthenticationContext.empty()
.with(
MatchRule.ALL,
AuthenticationConfiguration.EMPTY
.useName(username == null ? "$local" : username)
.useRealm(null)
.usePassword(passwordsToUse.getOrDefault(username, ""))
.allowSaslMechanisms("DIGEST-MD5")
.useMechanismProperties(getSaslProperties(builder.getMap()))
.useProvidersFromClassLoader(SwitchIdentityTestCase.class.getClassLoader()));
return authenticationContext;
}
private Map<String, String> getSaslProperties(final OptionMap connectionCreationOptions) {
Map<String, String> saslProperties = null;
Sequence<Property> value = connectionCreationOptions.get(Options.SASL_PROPERTIES);
if (value != null) {
saslProperties = new HashMap<>(value.size());
for (Property property : value) {
saslProperties.put(property.getKey(), (String) property.getValue());
}
}
return saslProperties;
}
/**
* Perform the tests using the SecurityContextAssociation API to set the desired Principal.
*/
private void callUsingSecurityContextAssociation(String userName, boolean hasRole1, boolean hasRole2) throws Exception {
try {
final Properties ejbClientConfiguration = EJBUtil.createEjbClientConfiguration(Utils.getHost(mgmtClient), userName);
// register the client side interceptor
final EJBClientContext ejbClientContext = EJBClientContext.getCurrent().withAddedInterceptors(new ClientSecurityInterceptor());
SecurityContextAssociation.setPrincipal(new SimplePrincipal(userName));
ejbClientContext.runCallable(() -> {
final Manage targetBean = EJBUtil.lookupEJB(ejbClientConfiguration,TargetBean.class, Manage.class);
final Manage bridgeBean = EJBUtil.lookupEJB(ejbClientConfiguration, BridgeBean.class, Manage.class);
//test direct access
testMethodAccess(targetBean, ManageMethodEnum.ALLROLES, true);
testMethodAccess(targetBean, ManageMethodEnum.ROLE1, hasRole1);
testMethodAccess(targetBean, ManageMethodEnum.ROLE2, hasRole2);
//test security context propagation
testMethodAccess(bridgeBean, ManageMethodEnum.ALLROLES, true);
testMethodAccess(bridgeBean, ManageMethodEnum.ROLE1, hasRole1);
testMethodAccess(bridgeBean, ManageMethodEnum.ROLE2, hasRole2);
return null;
});
} finally {
SecurityContextAssociation.clearSecurityContext();
}
}
/**
* Tests access to a single method of a {@link Manage} EJB implementation.
*
* @param bean EJB instance
* @param method method type
* @param hasAccess expected value
*/
private void testMethodAccess(Manage bean, ManageMethodEnum method, boolean hasAccess) {
try {
final String result;
switch (method) {
case ROLE1:
result = bean.role1();
break;
case ROLE2:
result = bean.role2();
break;
case ALLROLES:
result = bean.allRoles();
break;
default:
throw new IllegalArgumentException("Unknown bean method type.");
}
assertEquals(Manage.RESULT, result);
if (!hasAccess) {
fail("Acess should be denied.");
}
} catch (EJBAccessException e) {
if (hasAccess) {
fail("Access should be allowed.");
}
}
}
// Embedded classes ------------------------------------------------------
/**
* A {@link ServerSetupTask} instance which creates security domains for this test case.
*
* @author Josef Cacek
*/
static class SecurityDomainsSetup extends AbstractSecurityDomainsServerSetupTask {
/**
* Returns SecurityDomains configuration for this testcase.
*
* <pre>
* <security-domain name="switch-identity-test" cache-type="default">
* <authentication>
* <login-module code="{@link GuestDelegationLoginModule}" flag="optional">
* <module-option name="password-stacking" value="useFirstPass"/>
* </login-module>
* <login-module code="Remoting" flag="optional">
* <module-option name="password-stacking" value="useFirstPass"/>
* </login-module>
* <login-module code="RealmDirect" flag="required">
* <module-option name="password-stacking" value="useFirstPass"/>
* </login-module>
* </authentication>
* </security-domain>
* </pre>
*
* @see org.jboss.as.test.integration.security.common.AbstractSecurityDomainsServerSetupTask#getSecurityDomains()
*/
@Override
protected SecurityDomain[] getSecurityDomains() {
final SecurityModule.Builder loginModuleBuilder = new SecurityModule.Builder().flag("optional").putOption(
"password-stacking", "useFirstPass");
final SecurityDomain sd = new SecurityDomain.Builder()
.name(SECURITY_DOMAIN_NAME)
.loginModules(loginModuleBuilder.name(GuestDelegationLoginModule.class.getName()).build(),
loginModuleBuilder.name("Remoting").build(), //
loginModuleBuilder.name("RealmDirect").build()) //
.build();
return new SecurityDomain[] { sd };
}
}
/**
* A {@link ServerSetupTask} instance which creates security realms for this test case.
*
* @author Josef Cacek
*/
static class SecurityRealmsSetup extends AbstractSecurityRealmsServerSetupTask {
/**
* Returns SecurityRealms configuration for this testcase.
*
* <pre>
* <security-realm name="ejb-outbound-realm">
* <server-identities>
* <secret value="xxx"/>
* </server-identities>
* </security-realm>
* </pre>
*
*/
@Override
protected SecurityRealm[] getSecurityRealms() {
final ServerIdentity serverIdentity = new ServerIdentity.Builder().secretPlain(EJBUtil.CONNECTION_PASSWORD).build();
final SecurityRealm realm = new SecurityRealm.Builder().name(EJB_OUTBOUND_REALM).serverIdentity(serverIdentity)
.build();
return new SecurityRealm[] { realm };
}
}
/**
* An Enum, which holds expected method types in {@link Manage} interface.
*
* @author Josef Cacek
*/
private enum ManageMethodEnum {
ROLE1, ROLE2, ALLROLES
}
}