/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, 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.ejb3.iiop; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.rmi.RemoteException; import java.security.AccessController; import java.security.Principal; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.Map; import javax.ejb.EJBMetaData; import javax.ejb.HomeHandle; import javax.management.MBeanException; import javax.transaction.Status; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.jboss.as.ee.component.Component; import org.jboss.as.ee.component.ComponentView; import org.jboss.as.ejb3.component.stateful.StatefulSessionComponent; import org.jboss.as.ejb3.logging.EjbLogger; import org.jboss.iiop.csiv2.SASCurrent; import org.jboss.as.naming.context.NamespaceContextSelector; import org.jboss.ejb.client.SessionID; import org.jboss.ejb.iiop.HandleImplIIOP; import org.jboss.invocation.InterceptorContext; import org.jboss.marshalling.InputStreamByteInput; import org.jboss.marshalling.MarshallerFactory; import org.jboss.marshalling.MarshallingConfiguration; import org.jboss.marshalling.Unmarshaller; import org.jboss.security.SecurityContext; import org.jboss.security.SecurityContextAssociation; import org.jboss.security.SecurityContextFactory; import org.omg.CORBA.BAD_OPERATION; import org.omg.CORBA.InterfaceDef; import org.omg.CORBA.ORB; import org.omg.CORBA.ORBPackage.InvalidName; import org.omg.CORBA.portable.InputStream; import org.omg.CORBA.portable.InvokeHandler; import org.omg.CORBA.portable.OutputStream; import org.omg.CORBA.portable.ResponseHandler; import org.omg.PortableServer.Current; import org.omg.PortableServer.CurrentPackage.NoContext; import org.omg.PortableServer.POA; import org.omg.PortableServer.Servant; import org.wildfly.iiop.openjdk.rmi.RmiIdlUtil; import org.wildfly.iiop.openjdk.rmi.marshal.strategy.SkeletonStrategy; import org.wildfly.security.auth.client.AuthenticationConfiguration; import org.wildfly.security.auth.client.AuthenticationContext; import org.wildfly.security.auth.client.MatchRule; import org.wildfly.security.auth.principal.NamePrincipal; 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.auth.server.ServerAuthenticationContext; import org.wildfly.security.evidence.PasswordGuessEvidence; import org.wildfly.security.manager.WildFlySecurityManager; /** * CORBA servant class for the <code>EJBObject</code>s of a given bean. An * instance of this class "implements" the bean's set of <code>EJBObject</code> * instances by forwarding to the bean container all IIOP invocations on any * of the bean's <code>EJBObject</code>s. * * @author <a href="mailto:reverbel@ime.usp.br">Francisco Reverbel</a> * @author Stuart Douglas */ public class EjbCorbaServant extends Servant implements InvokeHandler, LocalIIOPInvoker { /** * The injected component view */ private final ComponentView componentView; /** * The ORB */ private final ORB orb; /** * Thread-local <code>Current</code> object from which we get the target oid * in an incoming IIOP request. */ private final Current poaCurrent; /** * Mapping from bean methods to <code>SkeletonStrategy</code> instances. */ private final Map<String, SkeletonStrategy> methodInvokerMap; /** * CORBA repository ids of the RMI-IDL interfaces implemented by the bean * (<code>EJBObject</code> instance). */ private final String[] repositoryIds; /** * CORBA reference to an IR object representing the bean's remote interface. */ private final InterfaceDef interfaceDef; /** * The security domain for CORBA invocations */ private final String legacySecurityDomain; /** * The Elytron security domain for CORBA invocations */ private final SecurityDomain securityDomain; /** * If true this is the servant for an EJBHome object */ private final boolean home; /** * <code>HomeHandle</code> for the <code>EJBHome</code> * implemented by this servant. */ private volatile HomeHandle homeHandle = null; /** * The metadata for this */ private volatile EJBMetaData ejbMetaData; /** * A reference to the SASCurrent, or null if the SAS interceptors are not * installed. */ private final SASCurrent sasCurrent; /** * A reference to the InboundTransactionCurrent, or null if OTS interceptors * are not installed. */ private final org.jboss.iiop.tm.InboundTransactionCurrent inboundTxCurrent; /** * The transaction manager */ private final TransactionManager transactionManager; /** * Used for serializing EJB id's */ private final MarshallerFactory factory; private final MarshallingConfiguration configuration; /** * The EJB's deployment class loader */ private final ClassLoader classLoader; /** * Constructs an <code>EjbObjectCorbaServant></code>. */ public EjbCorbaServant(final Current poaCurrent, final Map<String, SkeletonStrategy> methodInvokerMap, final String[] repositoryIds, final InterfaceDef interfaceDef, final ORB orb, final ComponentView componentView, final MarshallerFactory factory, final MarshallingConfiguration configuration, final TransactionManager transactionManager, final ClassLoader classLoader, final boolean home, final String legacySecurityDomain, final SecurityDomain securityDomain) { this.poaCurrent = poaCurrent; this.methodInvokerMap = methodInvokerMap; this.repositoryIds = repositoryIds; this.interfaceDef = interfaceDef; this.orb = orb; this.componentView = componentView; this.factory = factory; this.configuration = configuration; this.transactionManager = transactionManager; this.classLoader = classLoader; this.home = home; this.legacySecurityDomain = legacySecurityDomain; this.securityDomain = securityDomain; SASCurrent sasCurrent; try { sasCurrent = (SASCurrent) this.orb.resolve_initial_references("SASCurrent"); } catch (InvalidName invalidName) { sasCurrent = null; } this.sasCurrent = sasCurrent; org.jboss.iiop.tm.InboundTransactionCurrent inboundTxCurrent; try { inboundTxCurrent = (org.jboss.iiop.tm.InboundTransactionCurrent) this.orb.resolve_initial_references(org.jboss.iiop.tm.InboundTransactionCurrent.NAME); } catch (InvalidName invalidName) { inboundTxCurrent = null; } this.inboundTxCurrent = inboundTxCurrent; } /** * Returns an IR object describing the bean's remote interface. */ public org.omg.CORBA.Object _get_interface_def() { if (interfaceDef != null) return interfaceDef; else return super._get_interface_def(); } /** * Returns an array with the CORBA repository ids of the RMI-IDL interfaces * implemented by this servant's <code>EJBObject</code>s. */ public String[] _all_interfaces(POA poa, byte[] objectId) { return repositoryIds.clone(); } /** * Receives IIOP requests to this servant's <code>EJBObject</code>s * and forwards them to the bean container, through the JBoss * <code>MBean</code> server. */ public OutputStream _invoke(final String opName, final InputStream in, final ResponseHandler handler) { EjbLogger.ROOT_LOGGER.tracef("EJBObject invocation: %s", opName); SkeletonStrategy op = methodInvokerMap.get(opName); if (op == null) { EjbLogger.ROOT_LOGGER.debugf("Unable to find opname '%s' valid operations:%s", opName, methodInvokerMap.keySet()); throw new BAD_OPERATION(opName); } final NamespaceContextSelector selector = componentView.getComponent().getNamespaceContextSelector(); final ClassLoader oldCl = WildFlySecurityManager.getCurrentContextClassLoaderPrivileged(); NamespaceContextSelector.pushCurrentSelector(selector); try { WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(classLoader); org.omg.CORBA_2_3.portable.OutputStream out; try { Object retVal; if (!home && opName.equals("_get_handle")) { retVal = new HandleImplIIOP(orb.object_to_string(_this_object())); } else if (home && opName.equals("_get_homeHandle")) { retVal = homeHandle; } else if (home && opName.equals("_get_EJBMetaData")) { retVal = ejbMetaData; } else { Principal identityPrincipal = null; Principal principal = null; Object credential = null; if (this.sasCurrent != null) { final byte[] incomingIdentity = this.sasCurrent.get_incoming_principal_name(); //we have an identity token, which is a trust based mechanism if (incomingIdentity != null && incomingIdentity.length > 0) { String name = new String(incomingIdentity, StandardCharsets.UTF_8); int domainIndex = name.indexOf('@'); if (domainIndex > 0) name = name.substring(0, domainIndex); identityPrincipal = new NamePrincipal(name); } final byte[] incomingUsername = this.sasCurrent.get_incoming_username(); if (incomingUsername != null && incomingUsername.length > 0) { final byte[] incomingPassword = this.sasCurrent.get_incoming_password(); String name = new String(incomingUsername, StandardCharsets.UTF_8); int domainIndex = name.indexOf('@'); if (domainIndex > 0) { name = name.substring(0, domainIndex); } principal = new NamePrincipal(name); credential = new String(incomingPassword, StandardCharsets.UTF_8).toCharArray(); } } final Object[] params = op.readParams((org.omg.CORBA_2_3.portable.InputStream) in); if (!this.home && opName.equals("isIdentical") && params.length == 1) { //handle isIdentical specially Object val = params[0]; retVal = val instanceof org.omg.CORBA.Object && handleIsIdentical((org.omg.CORBA.Object) val); } else { if (this.securityDomain != null) { // an elytron security domain is available: authenticate and authorize the client before invoking the component. SecurityIdentity identity = this.securityDomain.getAnonymousSecurityIdentity(); AuthenticationConfiguration authenticationConfiguration = AuthenticationConfiguration.EMPTY; if (identityPrincipal != null) { // we have an identity token principal - check if the TLS identity, if available, // has permission to run as the identity token principal. // TODO use the TLS identity when that becomes available to us. // no TLS identity found, check if an initial context token was also sent. If it was, // authenticate the incoming username/password and check if the resulting identity has // permission to run as the identity token principal. if (principal != null) { char[] password = (char[]) credential; authenticationConfiguration = authenticationConfiguration.useName(principal.getName()) .usePassword(password); SecurityIdentity authenticatedIdentity = this.authenticate(principal, password); identity = authenticatedIdentity.createRunAsIdentity(identityPrincipal.getName(), true); } else { // no TLS nor initial context token found - check if the anonymous identity has // permission to run as the identity principal. identity = this.securityDomain.getAnonymousSecurityIdentity().createRunAsIdentity(identityPrincipal.getName(), true); } } else if (principal != null) { char[] password = (char[]) credential; // we have an initial context token containing a username/password pair. authenticationConfiguration = authenticationConfiguration.useName(principal.getName()) .usePassword(password); identity = this.authenticate(principal, password); } final InterceptorContext interceptorContext = new InterceptorContext(); this.prepareInterceptorContext(op, params, interceptorContext); try { final AuthenticationContext context = AuthenticationContext.captureCurrent().with(MatchRule.ALL.matchProtocol("iiop"), authenticationConfiguration); retVal = identity.runAs((PrivilegedExceptionAction<Object>) () -> context.run((PrivilegedExceptionAction<Object>) () -> this.componentView.invoke(interceptorContext))); } catch (PrivilegedActionException e) { throw e.getCause(); } } else { // legacy security behavior: setup the security context if a SASCurrent is available and invoke the component. // One of the EJB security interceptors will authenticate and authorize the client. SecurityContext legacyContext = null; if (this.legacySecurityDomain != null && (identityPrincipal != null || principal != null)) { // we don't have any real way to establish trust in identity based auth so we just use // the SASCurrent as a credential, and a custom legacy login module can make a decision for us. final Object finalCredential = identityPrincipal != null ? this.sasCurrent : credential; final Principal finalPrincipal = identityPrincipal != null ? identityPrincipal : principal; if (WildFlySecurityManager.isChecking()) { legacyContext = AccessController.doPrivileged((PrivilegedExceptionAction<SecurityContext>) () -> { SecurityContext sc = SecurityContextFactory.createSecurityContext(this.legacySecurityDomain); sc.getUtil().createSubjectInfo(finalPrincipal, finalCredential, null); return sc; }); } else { legacyContext = SecurityContextFactory.createSecurityContext(this.legacySecurityDomain); legacyContext.getUtil().createSubjectInfo(finalPrincipal, finalCredential, null); } } if (legacyContext != null) { setSecurityContextOnAssociation(legacyContext); } try { final InterceptorContext interceptorContext = new InterceptorContext(); if (legacyContext != null) { interceptorContext.putPrivateData(SecurityContext.class, legacyContext); } prepareInterceptorContext(op, params, interceptorContext); retVal = this.componentView.invoke(interceptorContext); } finally { if (legacyContext != null) { clearSecurityContextOnAssociation(); } } } } } out = (org.omg.CORBA_2_3.portable.OutputStream) handler.createReply(); if (op.isNonVoid()) { op.writeRetval(out, retVal); } } catch (Throwable e) { EjbLogger.ROOT_LOGGER.trace("Exception in EJBObject invocation", e); if (e instanceof MBeanException) { e = ((MBeanException) e).getTargetException(); } RmiIdlUtil.rethrowIfCorbaSystemException(e); out = (org.omg.CORBA_2_3.portable.OutputStream) handler.createExceptionReply(); op.writeException(out, e); } return out; } finally { NamespaceContextSelector.popCurrentSelector(); WildFlySecurityManager.setCurrentContextClassLoaderPrivileged(oldCl); } } private void prepareInterceptorContext(final SkeletonStrategy op, final Object[] params, final InterceptorContext interceptorContext) throws IOException, ClassNotFoundException { if (!home) { if (componentView.getComponent() instanceof StatefulSessionComponent) { final SessionID sessionID = (SessionID) unmarshalIdentifier(); interceptorContext.putPrivateData(SessionID.class, sessionID); } } interceptorContext.setContextData(new HashMap<>()); interceptorContext.setParameters(params); interceptorContext.setMethod(op.getMethod()); interceptorContext.putPrivateData(ComponentView.class, componentView); interceptorContext.putPrivateData(Component.class, componentView.getComponent()); interceptorContext.setTransaction(inboundTxCurrent == null ? null : inboundTxCurrent.getCurrentTransaction()); } private boolean handleIsIdentical(final org.omg.CORBA.Object val) throws RemoteException { //TODO: is this correct? return orb.object_to_string(_this_object()).equals(orb.object_to_string(val)); } private Object unmarshalIdentifier() throws IOException, ClassNotFoundException { final Object id; try { final byte[] idData = poaCurrent.get_object_id(); final Unmarshaller unmarshaller = factory.createUnmarshaller(configuration); unmarshaller.start(new InputStreamByteInput(new ByteArrayInputStream(idData))); id = unmarshaller.readObject(); unmarshaller.finish(); } catch (NoContext noContext) { throw new RuntimeException(noContext); } return id; } // Implementation of the interface LocalIIOPInvoker ------------------------ /** * Receives intra-VM invocations on this servant's <code>EJBObject</code>s * and forwards them to the bean container, through the JBoss * <code>MBean</code> * server. */ public Object invoke(String opName, Object[] arguments, Transaction tx, Principal identity, Object credential) throws Exception { EjbLogger.ROOT_LOGGER.tracef("EJBObject local invocation: %s", opName); SkeletonStrategy op = methodInvokerMap.get(opName); if (op == null) { throw new BAD_OPERATION(opName); } if (tx != null) { transactionManager.resume(tx); } try { final InterceptorContext interceptorContext = new InterceptorContext(); prepareInterceptorContext(op, arguments, interceptorContext); return componentView.invoke(interceptorContext); } finally { if (tx != null) { if (transactionManager.getStatus() != Status.STATUS_NO_TRANSACTION) { transactionManager.suspend(); } } } } public void setHomeHandle(final HomeHandle homeHandle) { this.homeHandle = homeHandle; } public void setEjbMetaData(final EJBMetaData ejbMetaData) { this.ejbMetaData = ejbMetaData; } private static void setSecurityContextOnAssociation(final SecurityContext sc) { AccessController.doPrivileged((PrivilegedAction<Void>) () -> { SecurityContextAssociation.setSecurityContext(sc); return null; }); } private static void clearSecurityContextOnAssociation() { AccessController.doPrivileged((PrivilegedAction<Void>) () -> { SecurityContextAssociation.clearSecurityContext(); return null; }); } /** * Authenticate the user with the given credential against the configured Elytron security domain. * * @param principal the principal representing the user being authenticated. * @param credential the credential used as evidence to verify the user's identity. * @return the authenticated and authorized {@link SecurityIdentity}. * @throws Exception if an error occurs while authenticating the user. */ private SecurityIdentity authenticate(final Principal principal, final char[] credential) throws Exception { final ServerAuthenticationContext context = this.securityDomain.createNewAuthenticationContext(); final PasswordGuessEvidence evidence = new PasswordGuessEvidence(credential != null ? credential : null); try { context.setAuthenticationPrincipal(principal); if (context.verifyEvidence(evidence)) { if (context.authorize()) { context.succeed(); return context.getAuthorizedIdentity(); } else { context.fail(); throw new SecurityException("Authorization failed"); } } else { context.fail(); throw new SecurityException("Authentication failed"); } } catch (IllegalArgumentException | IllegalStateException | RealmUnavailableException e) { context.fail(); throw e; } finally { evidence.destroy(); } } }