/* * 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.ejb.plugins; import static org.jboss.security.SecurityConstants.DEFAULT_EJB_APPLICATION_POLICY; import java.lang.reflect.Method; import java.security.CodeSource; import java.security.Principal; import java.util.Map; import java.util.Set; import javax.ejb.TimedObject; import javax.ejb.Timer; import javax.security.auth.Subject; import org.jboss.ejb.Container; import org.jboss.invocation.Invocation; import org.jboss.metadata.ApplicationMetaData; import org.jboss.metadata.AssemblyDescriptorMetaData; import org.jboss.metadata.BeanMetaData; import org.jboss.metadata.SecurityIdentityMetaData; import org.jboss.security.AuthenticationManager; import org.jboss.security.ISecurityManagement; import org.jboss.security.RealmMapping; import org.jboss.security.RunAs; import org.jboss.security.RunAsIdentity; import org.jboss.security.SecurityContext; import org.jboss.security.SecurityRolesAssociation; import org.jboss.security.SecurityUtil; import org.jboss.security.identity.plugins.SimpleRoleGroup; import org.jboss.security.javaee.AbstractEJBAuthorizationHelper; import org.jboss.security.javaee.EJBAuthenticationHelper; import org.jboss.security.javaee.SecurityHelperFactory; import org.jboss.system.Registry; /** * The SecurityInterceptor is where the EJB 2.0 declarative security model * is enforced. This is where the caller identity propagation is controlled as well. * * @author <a href="on@ibis.odessa.ua">Oleg Nitz</a> * @author <a href="mailto:Scott.Stark@jboss.org">Scott Stark</a>. * @author <a href="mailto:Thomas.Diesler@jboss.org">Thomas Diesler</a>. * @author <a href="mailto:Anil.Saldhana@jboss.org">Anil Saldhana</a> * @version $Revision: 103480 $ */ public class SecurityInterceptor extends AbstractInterceptor { /** The interface of an observer that should be notified when principal authentication fails. */ public interface AuthenticationObserver { final String KEY = "SecurityInterceptor.AuthenticationObserver"; void authenticationFailed(); } /** The authentication manager plugin */ protected AuthenticationManager securityManager; /** The authorization manager plugin */ protected RealmMapping realmMapping; // The bean uses this run-as identity to call out protected RunAs runAsIdentity; // A map of SecurityRolesMetaData from jboss.xml protected Map securityRoles; //A map of principal versus roles from jboss-app.xml/jboss.xml protected Map<String, Set<String>> deploymentRoles; // The observer to be notified when principal authentication fails. // This is a hook for the CSIv2 code. The authenticationObserver may // send out a ContextError message, as required by the CSIv2 protocol. protected AuthenticationObserver authenticationObserver; /** The TimedObject.ejbTimeout callback */ protected Method ejbTimeout; //Authorization Framework changes protected String ejbName = null; protected CodeSource ejbCS = null; /** * Security Domain configured as part of the application */ protected String appSecurityDomain = null; //Fallback Security Domain protected String defaultAuthorizationSecurityDomain = DEFAULT_EJB_APPLICATION_POLICY; /** * Specify whether <use-caller-identity> is configured, mainly * for the use case of caller identity coming with run-as */ protected boolean isUseCallerIdentity = false; /** * Represents the holder of the various security managers * configured at the container level */ protected ISecurityManagement securityManagement = null; /** Called by the super class to set the container to which this interceptor belongs. We obtain the security manager and runAs identity to use here. */ public void setContainer(Container container) { super.setContainer(container); if (container != null) { BeanMetaData beanMetaData = container.getBeanMetaData(); ApplicationMetaData applicationMetaData = beanMetaData.getApplicationMetaData(); AssemblyDescriptorMetaData assemblyDescriptor = applicationMetaData.getAssemblyDescriptor(); securityRoles = assemblyDescriptor.getSecurityRoles(); deploymentRoles = assemblyDescriptor.getPrincipalVersusRolesMap(); SecurityIdentityMetaData secMetaData = beanMetaData.getSecurityIdentityMetaData(); if (secMetaData != null && secMetaData.getUseCallerIdentity() == false) { String roleName = secMetaData.getRunAsRoleName(); String principalName = secMetaData.getRunAsPrincipalName(); //Special Case: if RunAsPrincipal is not configured, then we use unauthenticatedIdentity if (principalName == null) principalName = applicationMetaData.getUnauthenticatedPrincipal(); // the run-as principal might have extra roles mapped in the assembly-descriptor Set extraRoleNames = assemblyDescriptor.getSecurityRoleNamesByPrincipal(principalName); runAsIdentity = new RunAsIdentity(roleName, principalName, extraRoleNames); } if (secMetaData != null && secMetaData.getUseCallerIdentity()) this.isUseCallerIdentity = true; securityManager = container.getSecurityManager(); realmMapping = container.getRealmMapping(); //authorizationManager = container.getAuthorizationManager(); try { // Get the timeout method ejbTimeout = TimedObject.class.getMethod("ejbTimeout", new Class[] {Timer.class}); } catch (NoSuchMethodException ignore) { } if (securityManager != null) { appSecurityDomain = securityManager.getSecurityDomain(); appSecurityDomain = SecurityUtil.unprefixSecurityDomain(appSecurityDomain); } ejbName = beanMetaData.getEjbName(); ejbCS = container.getBeanClass().getProtectionDomain().getCodeSource(); securityManagement = (ISecurityManagement) container.getSecurityManagement(); } } // Container implementation -------------------------------------- public void start() throws Exception { super.start(); authenticationObserver = (AuthenticationObserver) Registry.lookup(AuthenticationObserver.KEY); //Take care of hot deployed security domains if (container != null) { securityManager = container.getSecurityManager(); if (securityManager != null) { appSecurityDomain = securityManager.getSecurityDomain(); appSecurityDomain = SecurityUtil.unprefixSecurityDomain(appSecurityDomain); } } } public Object invokeHome(Invocation mi) throws Exception { boolean isInvoke = false; return process(mi, isInvoke); } public Object invoke(Invocation mi) throws Exception { boolean isInvoke = true; return process(mi, isInvoke); } /** * Process the invocation * @param mi * @param isInvoke Are we from the invoke method? False = invokeHome method * @return * @throws Exception */ private Object process(Invocation mi, boolean isInvoke) throws Exception { if (this.shouldBypassSecurity(mi)) { if (log.isTraceEnabled()) log.trace("Bypass security for invoke or invokeHome"); if (isInvoke) return getNext().invoke(mi); else return getNext().invokeHome(mi); } SecurityContext sc = SecurityActions.getSecurityContext(); if (sc == null) throw new IllegalStateException("Security Context is null"); RunAs callerRunAsIdentity = sc.getIncomingRunAs(); if (log.isTraceEnabled()) log.trace("Caller RunAs=" + callerRunAsIdentity + ": useCallerIdentity=" + this.isUseCallerIdentity); // Authenticate the subject and apply any declarative security checks try { checkSecurityContext(mi, callerRunAsIdentity); } catch (Exception e) { log.error("Error in Security Interceptor", e); throw e; } /** * Special case: if <use-caller-identity> configured and * the caller is arriving with a run-as, we need to push that run-as */ if (callerRunAsIdentity != null && this.isUseCallerIdentity) this.runAsIdentity = callerRunAsIdentity; /* If a run-as role was specified, push it so that any calls made by this bean will have the runAsRole available for declarative security checks. */ SecurityActions.pushRunAsIdentity(runAsIdentity); try { if (isInvoke) return getNext().invoke(mi); else return getNext().invokeHome(mi); } finally { SecurityActions.popRunAsIdentity(); SecurityActions.popSubjectContext(); } } /** The EJB 2.0 declarative security algorithm: 1. Authenticate the caller using the principal and credentials in the MethodInvocation 2. Validate access to the method by checking the principal's roles against those required to access the method. */ private void checkSecurityContext(Invocation mi, RunAs callerRunAsIdentity) throws Exception { Principal principal = mi.getPrincipal(); Object credential = mi.getCredential(); boolean trace = log.isTraceEnabled(); // If there is not a security manager then there is no authentication required Method m = mi.getMethod(); boolean containerMethod = m == null || m.equals(ejbTimeout); if (containerMethod == true || securityManager == null || container == null) { // Allow for the propagation of caller info to other beans SecurityActions.pushSubjectContext(principal, credential, null); return; } if (realmMapping == null) { throw new SecurityException("Role mapping manager has not been set"); } SecurityContext sc = SecurityActions.getSecurityContext(); EJBAuthenticationHelper helper = SecurityHelperFactory.getEJBAuthenticationHelper(sc); boolean isTrusted = containsTrustableRunAs(sc) || helper.isTrusted(); if (!isTrusted) { // Check the security info from the method invocation Subject subject = new Subject(); if (SecurityActions.isValid(helper, subject, m.getName()) == false) { // Notify authentication observer if (authenticationObserver != null) authenticationObserver.authenticationFailed(); // Else throw a generic SecurityException String msg = "Authentication exception, principal=" + principal; throw new SecurityException(msg); } else { SecurityActions.pushSubjectContext(principal, credential, subject); if (trace) { log.trace("Authenticated principal=" + principal + " in security domain=" + sc.getSecurityDomain()); } } } else { // Duplicate the current subject context on the stack since //SecurityActions.dupSubjectContext(); SecurityActions.pushRunAsIdentity(callerRunAsIdentity); } Method ejbMethod = mi.getMethod(); // Ignore internal container calls if (ejbMethod == null) return; // Get the caller Subject caller = SecurityActions.getContextSubject(); if (caller == null) throw new IllegalStateException("Authenticated User. But caller subject is null"); //Establish the deployment rolename-principalset custom mapping(if available) SecurityRolesAssociation.setSecurityRoles(this.deploymentRoles); boolean isAuthorized = false; Set<Principal> methodRoles = container.getMethodPermissions(ejbMethod, mi.getType()); SecurityContext currentSC = SecurityActions.getSecurityContext(); if (SecurityActions.getSecurityManagement(currentSC) == null) SecurityActions.setSecurityManagement(currentSC, securityManagement); AbstractEJBAuthorizationHelper authorizationHelper = SecurityHelperFactory.getEJBAuthorizationHelper(sc); authorizationHelper.setPolicyRegistration(container.getPolicyRegistration()); isAuthorized = SecurityActions.authorize(authorizationHelper, ejbName, ejbMethod, mi.getPrincipal(), mi.getType().toInterfaceString(), ejbCS, caller, callerRunAsIdentity, container.getJaccContextID(), new SimpleRoleGroup(methodRoles)); if (!isAuthorized) { String msg = "Denied: caller with subject=" + caller + " and security context post-mapping roles=" + SecurityActions.getRolesFromSecurityContext(currentSC) + ": ejbMethod=" + ejbMethod; throw new SecurityException(msg); } } private boolean shouldBypassSecurity(Invocation mi) throws Exception { // If there is not a security manager then there is no authentication required Method m = mi.getMethod(); boolean containerMethod = m == null || m.equals(ejbTimeout); if (containerMethod == true || securityManager == null || container == null) { // Allow for the propagation of caller info to other beans SecurityActions.createAndSetSecurityContext(mi.getPrincipal(), mi.getCredential(), "BYPASSED-SECURITY"); if (this.runAsIdentity != null) SecurityActions.pushRunAsIdentity(runAsIdentity); return true; } return false; } private boolean containsTrustableRunAs(SecurityContext sc) { RunAs incomingRunAs = sc.getIncomingRunAs(); return incomingRunAs != null && incomingRunAs instanceof RunAsIdentity; } }