All Downloads are FREE. Search and download functionalities are using the official Maven repository.

edu.internet2.middleware.grouper.ws.GrouperServiceJ2ee Maven / Gradle / Ivy

There is a newer version: 5.13.5
Show newest version
/*******************************************************************************
 * Copyright 2012 Internet2
 * 
 * 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 edu.internet2.middleware.grouper.ws;

import java.io.IOException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.logging.log4j.ThreadContext;

import edu.internet2.middleware.grouper.Group;
import edu.internet2.middleware.grouper.GroupFinder;
import edu.internet2.middleware.grouper.GroupSave;
import edu.internet2.middleware.grouper.GrouperSession;
import edu.internet2.middleware.grouper.Member;
import edu.internet2.middleware.grouper.MemberFinder;
import edu.internet2.middleware.grouper.SubjectFinder;
import edu.internet2.middleware.grouper.audit.GrouperEngineBuiltin;
import edu.internet2.middleware.grouper.authentication.GrouperOidc;
import edu.internet2.middleware.grouper.authentication.GrouperPassword;
import edu.internet2.middleware.grouper.authentication.GrouperPublicPrivateKeyJwt;
import edu.internet2.middleware.grouper.authentication.GrouperTrustedJwt;
import edu.internet2.middleware.grouper.cache.GrouperCache;
import edu.internet2.middleware.grouper.cfg.GrouperHibernateConfig;
import edu.internet2.middleware.grouper.exception.GroupNotFoundException;
import edu.internet2.middleware.grouper.exception.GrouperSessionException;
import edu.internet2.middleware.grouper.exception.SessionException;
import edu.internet2.middleware.grouper.hibernate.GrouperContext;
import edu.internet2.middleware.grouper.hooks.beans.GrouperContextTypeBuiltIn;
import edu.internet2.middleware.grouper.hooks.beans.HooksContext;
import edu.internet2.middleware.grouper.instrumentation.InstrumentationThread;
import edu.internet2.middleware.grouper.j2ee.Authentication;
import edu.internet2.middleware.grouper.j2ee.ServletRequestUtils;
import edu.internet2.middleware.grouper.misc.GrouperSessionHandler;
import edu.internet2.middleware.grouper.misc.GrouperStartup;
import edu.internet2.middleware.grouper.privs.PrivilegeHelper;
import edu.internet2.middleware.grouper.util.GrouperLogger;
import edu.internet2.middleware.grouper.util.GrouperLoggerState;
import edu.internet2.middleware.grouper.util.GrouperUtil;
import edu.internet2.middleware.grouper.ws.coresoap.WsSubjectLookup;
import edu.internet2.middleware.grouper.ws.exceptions.GrouperWsException;
import edu.internet2.middleware.grouper.ws.exceptions.WsInvalidQueryException;
import edu.internet2.middleware.grouper.ws.security.WsCustomAuthentication;
import edu.internet2.middleware.grouper.ws.security.WsGrouperDefaultAuthentication;
import edu.internet2.middleware.grouper.ws.util.GrouperWsLog;
import edu.internet2.middleware.grouper.ws.util.GrouperWsLongRunningLog;
import edu.internet2.middleware.grouperClient.collections.MultiKey;
import edu.internet2.middleware.subject.Subject;
import edu.internet2.middleware.subject.SubjectNotFoundException;

/**
 * Extend the servlet to get user info
 * 
 * @author mchyzer
 * 
 */
public class GrouperServiceJ2ee implements Filter {

  /** logger */
  private static final Log LOG = GrouperUtil.getLog(GrouperServiceJ2ee.class);
  
  /**
   * if in request, get the start time
   * @return the start time
   */
  public static long retrieveRequestStartMillis() {
    Long requestStartMillis = threadLocalRequestStartMillis.get();
    return GrouperUtil.longValue(requestStartMillis, 0);
  }

  /**
   * get a single parameter value for key.  If multiple exist, throw error
   * @param paramMap is the map of params.  will get value from here if no request object
   * @param httpServletRequest optional.  if there, will make sure no dupes
   * @param key to lookup
   * @return the value
   */
  public static String parameterValue(Map paramMap,
      HttpServletRequest httpServletRequest, String key) {
    //if no servlet (probably just testing), get from map
    if (httpServletRequest == null) {
      if (paramMap == null || paramMap.isEmpty() || !paramMap.containsKey(key)) {
        return null;
      }
      return paramMap.get(key)[0];
    }
    String[] values = httpServletRequest.getParameterValues(key);
    if (values == null || values.length == 0) {
      return null;
    }
    //there is probably something wrong if multiple values detected
    if (values.length > 1) {
      throw new RuntimeException(
          "Multiple request parameter values where detected for key: " + key
              + ", when only one is expected: " + GrouperUtil.toStringForLog(values));
    }
    return values[0];
  }

  /**
   * retrieve the user principal (who is authenticated) from the (threadlocal)
   * request object
   * 
   * @return the user principal name
   */
  public static String retrieveUserPrincipalNameFromRequest() {

    HttpServletRequest httpServletRequest = retrieveHttpServletRequest();
    GrouperUtil
        .assertion(httpServletRequest != null,
            "HttpServletRequest is null, is the GrouperServiceServlet mapped in the web.xml?");
    Principal principal = httpServletRequest.getUserPrincipal();
    String principalName = null;
    if (principal == null) {
      principalName = httpServletRequest.getRemoteUser();
      if (StringUtils.isBlank(principalName)) {
        principalName = (String)httpServletRequest.getAttribute("REMOTE_USER");
      }
    } else {
      principalName = principal.getName();
    }
    GrouperUtil.assertion(StringUtils.isNotBlank(principalName),
        "There is no user logged in, make sure the container requires authentication");
    return principalName;
  }

  /**
   * retrieve the subject logged in to web service
   * If there are four colons, then this is the source and subjectId since
   * overlap in namespace
   * 
   * @return the subject
   */
  @SuppressWarnings({ "unchecked", "deprecation" })
  public static Subject retrieveSubjectLoggedIn() {
    
    Map debugMap = GrouperServiceJ2ee.retrieveDebugMap();
    
    String authenticationClassName = GrouperWsConfig.getPropertyString(
        GrouperWsConfig.WS_SECURITY_NON_RAMPART_AUTHENTICATION_CLASS,
        WsGrouperDefaultAuthentication.class.getName());
    
    // add this in for JWT
    String userIdLoggedIn = (String)retrieveHttpServletRequest().getAttribute("REMOTE_USER");
    if (StringUtils.isBlank(userIdLoggedIn)) {
      if (wssecServlet()) {
  
        Class clazz = GrouperUtil.forName("edu.internet2.middleware.grouper.ws.security.RampartUtil");
        try {
          Method method = clazz.getMethod("getUserIdLoggedIn");
          userIdLoggedIn = (String)method.invoke(null);
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
        
        GrouperUtil.assertion(StringUtils.isNotBlank(userIdLoggedIn),
            "There is no Rampart user logged in, make sure the container requires authentication");
      } else {
        //this is for container auth (or custom auth, non-rampart)
        //get an instance
        Class theClass = GrouperUtil
            .forName(authenticationClassName);
  
        WsCustomAuthentication wsAuthentication = GrouperUtil.newInstance(theClass);
  
        userIdLoggedIn = wsAuthentication
            .retrieveLoggedInSubjectId(retrieveHttpServletRequest());
      }
    }
    
    // cant be blank!
    if (StringUtils.isBlank(userIdLoggedIn)) {
      //server is having trouble if got this far, but also the user's fault
      throw new WsInvalidQueryException("No user is logged in");
    }

    //null means dont look in a certain source
    String sourceId = null;

    //see if we need to split with 4 colons in login name
    if (StringUtils.contains(userIdLoggedIn, GrouperWsConfig.WS_SEPARATOR)) {
      String[] sourceSubjectId = GrouperUtil.splitTrim(userIdLoggedIn,
          GrouperWsConfig.WS_SEPARATOR);
      sourceId = sourceSubjectId[0];
      userIdLoggedIn = sourceSubjectId[1];
    } else {
      //see if there is a default source for all users to web service
      sourceId = StringUtils.trimToNull(GrouperWsConfig.retrieveConfig().propertyValueString(GrouperWsConfig.WS_LOGGED_IN_SUBJECT_DEFAULT_SOURCE));
    }

    String subjectIdPrefix = GrouperWsConfig
        .getPropertyString("ws.security.prependToUserIdForSubjectLookup");
    if (!StringUtils.isBlank(subjectIdPrefix)) {
      boolean skipDueToJwt = false;
      String authorizationHeader = retrieveHttpServletRequest().getHeader("Authorization");
      if (!StringUtils.isBlank(authorizationHeader) && authorizationHeader.startsWith("Bearer jwtUser_")) {
        skipDueToJwt = true;
      }
      if (!skipDueToJwt) {
        userIdLoggedIn = subjectIdPrefix + userIdLoggedIn;
      }
    }
    
    //puts it in the log4j ndc context so userid is logged
    if (ThreadContext.getDepth() == 0) {
      StringBuilder ndcBuilder = new StringBuilder("< ");
      if (!StringUtils.isBlank(sourceId)) {
        ndcBuilder.append(sourceId).append(" - ");
      }
      ndcBuilder.append(userIdLoggedIn).append(" - ");
      HttpServletRequest request = retrieveHttpServletRequest();
      if (request != null) {
        ndcBuilder.append(request.getRemoteAddr());
      }
      ndcBuilder.append(" >");
      ThreadContext.push(ndcBuilder.toString());
    }

    debugMap.put("userIdLoggedIn", userIdLoggedIn);
    
    Subject caller = null;
    GrouperSession grouperSession = GrouperSession.startRootSession(false);
    try {
      final String SOURCE_ID = sourceId;
      final String USER_ID_LOGGED_IN = userIdLoggedIn;
      caller = (Subject)GrouperSession.callbackGrouperSession(grouperSession, new GrouperSessionHandler() {

        public Object callback(GrouperSession theGrouperSession)
            throws GrouperSessionException {

          try {
            //see if across all sources
            if (SOURCE_ID == null) {
              return SubjectFinder.findByIdOrIdentifier(USER_ID_LOGGED_IN, true);
            }
            //see if in specified sources
            String[] sourceIds = GrouperUtil.splitTrim(SOURCE_ID, ",");
            for (String curSource : sourceIds) {
              Subject s = SubjectFinder.findByIdOrIdentifierAndSource(USER_ID_LOGGED_IN, curSource, false);
              if (s != null) {
                return s;
              }
            }
            throw new SubjectNotFoundException("Unable to find subject in source type(s): " + SOURCE_ID);
          } catch (Exception e) {
            //this is probably a system error...  not a user error
            throw new RuntimeException("Cant find subject from login id: " + USER_ID_LOGGED_IN, e);
          }
        }
      });

      Subject originalCaller = caller;
      if (caller != null) {
        debugMap.put("userIdLoggedInSource", caller.getSourceId());
        if (!StringUtils.equals(caller.getId(), (String)debugMap.get("userIdLoggedIn"))) {
          debugMap.put("userIdLoggedInSubjectId", caller.getId());
        }
      }

      caller = retrieveSubjectGrouperActAsHelper(caller);

      if (caller != null && originalCaller != null && !StringUtils.equals(caller.getId(), originalCaller.getId())) {
        debugMap.put("userIdActAsSource", caller.getSourceId());
        debugMap.put("userIdActAsSubjectId", caller.getId());
      }
      
      //this is set in filter
      GrouperContext grouperContext = GrouperContext.retrieveDefaultContext();
      
      Member member = MemberFinder.findBySubject(grouperSession, caller, true);
      
      grouperContext.setLoggedInMemberId(member.getUuid());
    } finally {
      GrouperSession.stopQuietly(grouperSession);
    }

    
    return caller;

  }

  /**
   * see if there is a grouper act as in play here
   * @param loggedInSubject
   * @return the subject
   */
  private static Subject retrieveSubjectGrouperActAsHelper(final Subject loggedInSubject) {
    
    //see if we are acting as someone else
    String grouperActAsGroup = GrouperWsConfig.retrieveConfig().propertyValueString("ws.grouper.act.as.group");
    if (StringUtils.isBlank(grouperActAsGroup)
        || loggedInSubject == null) {
      LOG.debug("No grouperActAs configured");
      return loggedInSubject;
    }

    GrouperSession session = null;
    
    HttpServletRequest httpServletRequest = retrieveHttpServletRequest();

    String grouperActAsSubjectId = httpServletRequest.getHeader("X-Grouper-actAsSubjectId");
    String grouperActAsSubjectIdentifier = httpServletRequest.getHeader("X-Grouper-actAsSubjectIdentifier");
    String grouperActAsSubjectSource = httpServletRequest.getHeader("X-Grouper-actAsSourceId");

    if (!StringUtils.isBlank(grouperActAsSubjectSource)) {

      if (!StringUtils.isBlank(grouperActAsSubjectId) 
          && !StringUtils.isBlank(grouperActAsSubjectIdentifier)) {
        throw new RuntimeException("You can only have one of X-Grouper-actAsSubjectId or X-Grouper-actAsSubjectIdentifier set!");
      }

      if (StringUtils.isBlank(grouperActAsSubjectId) 
          && StringUtils.isBlank(grouperActAsSubjectIdentifier)) {
        throw new RuntimeException("You must have one of X-Grouper-actAsSubjectId or X-Grouper-actAsSubjectIdentifier set if X-Grouper-actAsSourceId is set!");
      }

      try {
        grouperActAsSubjectSource = new String(new Base64().decode(grouperActAsSubjectSource.getBytes("UTF-8")), "UTF-8");
  
        if (!StringUtils.isBlank(grouperActAsSubjectId)) {
          grouperActAsSubjectId = new String(new Base64().decode(grouperActAsSubjectId.getBytes("UTF-8")), "UTF-8");
        } else if (!StringUtils.isBlank(grouperActAsSubjectIdentifier)) {
          grouperActAsSubjectIdentifier = new String(new Base64().decode(grouperActAsSubjectIdentifier.getBytes("UTF-8")), "UTF-8");
        }
      } catch (Exception e) {
        throw new RuntimeException("Problem with: source: '" + grouperActAsSubjectSource + "', id: '" 
            + grouperActAsSubjectId + "', or identifier: '" + grouperActAsSubjectIdentifier + "'", e);
      }
    } else {
      if (!StringUtils.isBlank(grouperActAsSubjectId) 
          || !StringUtils.isBlank(grouperActAsSubjectIdentifier)) {
        throw new RuntimeException("If X-Grouper-actAsSubjectId or X-Grouper-actAsSubjectIdentifier is set, then you must have a X-Grouper-actAsSourceId!");
      }
      //there is nothing there, go back
      return loggedInSubject;
    }
    
    // get the all powerful user
    Subject rootSubject = SubjectFinder.findRootSubject();

    try {
      session = GrouperSession.start(rootSubject);

      Subject actAsSubject = null;

      if (!StringUtils.isBlank(grouperActAsSubjectId)) {
        actAsSubject = SubjectFinder.findByIdAndSource(grouperActAsSubjectId, grouperActAsSubjectSource, true);
      } else if (!StringUtils.isBlank(grouperActAsSubjectIdentifier)) {
        actAsSubject = SubjectFinder.findByIdentifierAndSource(grouperActAsSubjectIdentifier, grouperActAsSubjectSource, true);
      } else {
        throw new RuntimeException("Why am I here?");
      }

      final Subject ACT_AS_SUBJECT = actAsSubject;
      
      //cache key to get or set if a user can act as another
      final MultiKey cacheKey = new MultiKey(loggedInSubject.getId(), loggedInSubject.getSource()
          .getId(), actAsSubject.getId(), actAsSubject.getSource().getId());

      Boolean inCache = null;
      
      if (actAsCacheMinutes() > 0) {
        inCache = grouperActAsCache().get(cacheKey);
      } else {
        inCache = false;
      }

      if (inCache != null && Boolean.TRUE.equals(inCache)) {
        LOG.debug("grouperActAs retrieved from cache");
        //if in cache and true, then allow
        return actAsSubject;
      }
      
      {
        //see if root or wheel group
        Subject rootAllowedSubject = (Subject)GrouperSession.callbackGrouperSession(session, new GrouperSessionHandler() {

          public Object callback(GrouperSession grouperSession) throws GrouperSessionException {
            if (PrivilegeHelper.isWheelOrRoot(loggedInSubject)) {
              actAsCache().put(cacheKey, Boolean.TRUE);
              if (LOG.isDebugEnabled()) {
                LOG.debug("grouperActAs allowed since logged in user is wheel or root: " + GrouperUtil.subjectToString(loggedInSubject));
              }
              return ACT_AS_SUBJECT;
            }
            return null;
          }
        });
  
        if (rootAllowedSubject != null) {
          return rootAllowedSubject;
        }
      }
      
      //first separate by comma
      String[] groupEntries = GrouperUtil.splitTrim(grouperActAsGroup, ",");

      //see if all throw exceptions
      int countNoExceptions = 0;

      //we could also cache which entries the user is in...  not sure how many entries will be here
      for (String groupEntry : groupEntries) {

        //each entry should be failsafe
        try {
          //now see if it is a multi input
          if (StringUtils.contains(groupEntry, GrouperWsConfig.WS_SEPARATOR)) {

            //it is the group the user is in, and the group the act as has to be in
            String[] groupEntryArray = GrouperUtil.splitTrim(groupEntry,
                GrouperWsConfig.WS_SEPARATOR);
            String userMustBeInGroupName = groupEntryArray[0];
            String actAsMustBeInGroupName = groupEntryArray[1];

            Group userMustBeInGroup = GroupFinder.findByName(session,
                userMustBeInGroupName, true);
            Group actAsMustBeInGroup = GroupFinder.findByName(session,
                actAsMustBeInGroupName, true);

            if (userMustBeInGroup.hasMember(loggedInSubject)
                && actAsMustBeInGroup.hasMember(actAsSubject)) {
              if (LOG.isDebugEnabled()) {
                LOG.debug("grouperActAs allowed since logged in user is in group: " + userMustBeInGroupName + ", and act as user is in group: " + actAsMustBeInGroupName);
              }
              //its ok, lets add to cache
              actAsCache().put(cacheKey, Boolean.TRUE);
              return actAsSubject;
            }

          } else {
            //else this is a straightforward rule where the logged in user just has to be in a group and
            //can act as anyone
            Group actAsGroup = GroupFinder.findByName(session, grouperActAsGroup, true);

            // if the logged in user is a member of the actAs group, then allow
            // the actAs
            if (actAsGroup.hasMember(loggedInSubject)) {
              if (LOG.isDebugEnabled()) {
                LOG.debug("grouperActAs allowed since logged in user is in group: " + grouperActAsGroup);
              }
              //its ok, lets add to cache
              actAsCache().put(cacheKey, Boolean.TRUE);
              // this is the subject the web service wants to use
              return actAsSubject;
            }
          }
          countNoExceptions++;
        } catch (Exception e) {
          //just log and dont act since other entries could be fine
          LOG.error("Problem with groupEntry: " + groupEntry + ", loggedInUser: "
              + loggedInSubject + ", actAsSubject: " + actAsSubject, e);
        }

      }

      if (countNoExceptions == 0) {
        throw new RuntimeException("Problems seeing if web service user '"
            + loggedInSubject + "' can actAs the other subject: '" + actAsSubject + "'");
      }
      // if not an effective member
      throw new RuntimeException(
          "A web service is specifying an actAsUser, but the groups specified in "
              + GrouperWsConfig.WS_ACT_AS_GROUP + " in the grouper-ws.properties "
              + " does not have a valid rule for member: '" + loggedInSubject
              + "', and actAs: '" + actAsSubject + "'");
    } catch (SessionException se) {
      throw new RuntimeException(se);
    } finally {
      GrouperSession.stopQuietly(session);
    }
    
  }
  
  /** cache the actAs */
  private static GrouperCache actAsCache = null;

  /** cache the grouper actAs */
  private static GrouperCache grouperActAsCache = null;
  
  /** cache the actAs */
  private static GrouperCache subjectAllowedCache = null;

  /**
   * get the actAsCache, and init if not initted
   * @return the actAsCache
   */
  private static GrouperCache actAsCache() {
    if (actAsCache == null) {
      int actAsTimeoutMinutes = actAsCacheMinutes();

      synchronized(GrouperServiceJ2ee.class) {
        if (actAsCache == null) {
          actAsCache = new GrouperCache(GrouperServiceJ2ee.class.getName() + "grouperWsActAsCache", 10000, false, 60*60*24, actAsTimeoutMinutes*60, false);
        }
      }
    }
    return actAsCache;
  }

  /**
   * get the grouperActAsCache, and init if not initted
   * @return the grouperActAsCache
   */
  private static GrouperCache grouperActAsCache() {
    if (grouperActAsCache == null) {
      int actAsTimeoutMinutes = actAsCacheMinutes();

      synchronized(GrouperServiceJ2ee.class) {
        if (grouperActAsCache == null) {
          grouperActAsCache = new GrouperCache(GrouperServiceJ2ee.class.getName() + "grouperGrouperWsActAsCache", 10000, false, 60*60*24, actAsTimeoutMinutes*60, false);
        }
      }
    }
    return grouperActAsCache;
  }

  /**
   * @return act as cache minutes
   */
  private static int actAsCacheMinutes() {
    int actAsTimeoutMinutes = GrouperWsConfig.retrieveConfig().propertyValueInt(
        GrouperWsConfig.WS_ACT_AS_CACHE_MINUTES, 5);
    return actAsTimeoutMinutes;
  }

  /**
   * get the subjectAllowedCache, and init if not initted
   * @return the subjectAllowedCache
   */
  private static GrouperCache subjectAllowedCache() {
    if (subjectAllowedCache == null) {
      int subjectAllowedTimeoutMinutes = GrouperWsConfig.retrieveConfig().propertyValueInt(
          GrouperWsConfig.WS_CLIENT_USER_GROUP_CACHE_MINUTES, 5);
      
      synchronized(GrouperServiceJ2ee.class) {
        if (subjectAllowedCache == null) {
          subjectAllowedCache = new GrouperCache(GrouperServiceJ2ee.class.getName() + "grouperWsAllowedCache", 10000, false, 60*60*24, subjectAllowedTimeoutMinutes*60, false);
        }
      }
    }
    return subjectAllowedCache;
  }

  /**
   * retrieve the subject to act as
   * 
   * @param actAsLookup that the caller wants to act as
   * @return the subject
   * @throws WsInvalidQueryException if there is a problem
   */
  public static Subject retrieveSubjectActAs(WsSubjectLookup actAsLookup)
      throws WsInvalidQueryException {
    Subject actAsSubject = retrieveSubjectActAsHelper(actAsLookup);
    HooksContext.assignSubjectActAs(actAsSubject);

    //this is set in filter
    GrouperContext grouperContext = GrouperContext.retrieveDefaultContext();

    GrouperSession grouperSession = GrouperSession.staticGrouperSession(false);
    GrouperSession rootSession = grouperSession == null ? 
        GrouperSession.startRootSession(false) : grouperSession.internal_getRootSession();

    
    Member member = MemberFinder.findBySubject(rootSession, actAsSubject, true);
    
    grouperContext.setLoggedInMemberIdActAs(member.getUuid());
    
    return actAsSubject;
  }

  /**
   * retrieve the subject to act as
   * 
   * @param actAsLookup that the caller wants to act as
   * @return the subject
   * @throws WsInvalidQueryException if there is a problem
   */
  private static Subject retrieveSubjectActAsHelper(WsSubjectLookup actAsLookup)
      throws WsInvalidQueryException {

    final String USER_IS_NOT_AUTHORIZED = "User is not authorized: ";

    final Subject loggedInSubject = retrieveSubjectLoggedIn();

    HooksContext.assignSubjectLoggedIn(loggedInSubject);
    
    //make sure allowed
    final String userGroupName = GrouperWsConfig.retrieveConfig().propertyValueString(GrouperWsConfig.WS_CLIENT_USER_GROUP_NAME);
    
    final String loggedInSubjectId = loggedInSubject.getId();
    if (!StringUtils.isBlank(userGroupName)) {
      GrouperSession grouperSession = null;
      
      try {
        //cache key to get or set if a user can act as another
        final MultiKey cacheKey = new MultiKey(loggedInSubjectId, 
            loggedInSubject.getSource().getId());

        Boolean allowedInCache = subjectAllowedCache().get(cacheKey);

        //if not in cache
        if (allowedInCache == null) {
          grouperSession = GrouperSession.startRootSession();
          GrouperSession.internal_callbackRootGrouperSession(new GrouperSessionHandler() {
            
            public Object callback(GrouperSession rootGrouperSession) throws GrouperSessionException {
              Group group = null;
              try {
                group = GroupFinder.findByName(rootGrouperSession, userGroupName, true);
              } catch (GroupNotFoundException gnfe) {
                group = new GroupSave().assignName(userGroupName).assignCreateParentStemsIfNotExist(true).save();
              }
              if (!group.hasMember(loggedInSubject)) {
                //not allowed, cache it
                subjectAllowedCache().put(cacheKey, false);
                throw new RuntimeException(USER_IS_NOT_AUTHORIZED + loggedInSubject + ", " + group);
              }
              subjectAllowedCache().put(cacheKey, true);
              return null;
            }
          });
        } else {
          //if in cache, reflect that
          if (!allowedInCache) {
            throw new RuntimeException(USER_IS_NOT_AUTHORIZED + loggedInSubject);
          }
        }
      } catch (Exception e) {
        String errorMessage = "user: '" + loggedInSubjectId + "' is not a member of group: '" + userGroupName 
            + "', and therefore is not authorized to use the app (configured in local grouper-ws.properties ws.client.user.group.name";
        if (e.getMessage().startsWith(USER_IS_NOT_AUTHORIZED)) {
          LOG.error(errorMessage);
          throw new GrouperWsException("User is not authorized", e).assignLogStack(false);
          
        }
        LOG.error(errorMessage, e);
        throw new GrouperWsException("User is not authorized", e);
      } finally {
        GrouperSession.stopQuietly(grouperSession);
      }
    }

    
    // if there is no actAs specified, then just use the logged in user
    if (actAsLookup == null || actAsLookup.blank()) {
      return loggedInSubject;
    }
    
    GrouperSession grouperSession = GrouperSession.startRootSession();
    Subject actAsSubject = null;
    try {
      actAsSubject = actAsLookup.retrieveSubject("actAsSubject");
    } finally {
      GrouperSession.stopQuietly(grouperSession);
    }
    
    //see if same:
    if (StringUtils.equals(loggedInSubjectId, actAsSubject.getId())
        && StringUtils.equals(loggedInSubject.getSource().getId(), actAsSubject.getSource().getId())) {
      return loggedInSubject;
    }
    
    //lets see if in cache    

    //cache key to get or set if a user can act as another
    MultiKey cacheKey = new MultiKey(loggedInSubjectId, loggedInSubject.getSource()
        .getId(), actAsSubject.getId(), actAsSubject.getSource().getId());

    Boolean inCache = null;
    
    if (actAsCacheMinutes() > 0) {
      inCache = actAsCache().get(cacheKey);
    } else {
      inCache = false;
    }

    if (inCache != null && Boolean.TRUE.equals(inCache)) {
      //if in cache and true, then allow
      return actAsSubject;
    }
    
    //see if root or wheel group
    GrouperSession session = null;
    try {
      session = GrouperSession.start(loggedInSubject);
      if (PrivilegeHelper.isRoot(session)) {
        actAsCache().put(cacheKey, Boolean.TRUE);
      return actAsSubject;
    }
    } catch (SessionException se) {
      throw new RuntimeException(se);
    } finally {
      GrouperSession.stopQuietly(session);
    }

    // so there is an actAs specified, lets see if we are allowed to use it
    // first lets get the group you have to be in if you are going to
    String actAsGroupName = GrouperWsConfig.retrieveConfig().propertyValueString(GrouperWsConfig.WS_ACT_AS_GROUP);

    // make sure there is one there
    if (StringUtils.isBlank(actAsGroupName)) {

      //if none configured, then probably a caller problem
      throw new WsInvalidQueryException(
          "A web service is specifying an actAsUser, but there is no '"
              + GrouperWsConfig.WS_ACT_AS_GROUP
              + "' specified in the grouper-ws.properties");
    }

    session = null;
    // get the all powerful user
    Subject rootSubject = SubjectFinder.findRootSubject();

    try {
      session = GrouperSession.start(rootSubject);

      //first separate by comma
      String[] groupEntries = GrouperUtil.splitTrim(actAsGroupName, ",");

      //see if all throw exceptions
      int countNoExceptions = 0;

      //we could also cache which entries the user is in...  not sure how many entries will be here
      for (String groupEntry : groupEntries) {

        //each entry should be failsafe
        try {
          //now see if it is a multi input
          if (StringUtils.contains(groupEntry, GrouperWsConfig.WS_SEPARATOR)) {

            //it is the group the user is in, and the group the act as has to be in
            String[] groupEntryArray = GrouperUtil.splitTrim(groupEntry,
                GrouperWsConfig.WS_SEPARATOR);
            String userMustBeInGroupName = groupEntryArray[0];
            String actAsMustBeInGroupName = groupEntryArray[1];

            Group userMustBeInGroup = GroupFinder.findByName(session,
                userMustBeInGroupName, true);
            Group actAsMustBeInGroup = GroupFinder.findByName(session,
                actAsMustBeInGroupName, true);

            if (userMustBeInGroup.hasMember(loggedInSubject)
                && actAsMustBeInGroup.hasMember(actAsSubject)) {
              //its ok, lets add to cache
              actAsCache().put(cacheKey, Boolean.TRUE);
              return actAsSubject;
            }

          } else {
            //else this is a straightforward rule where the logged in user just has to be in a group and
            //can act as anyone
            Group actAsGroup = GroupFinder.findByName(session, actAsGroupName, true);

            // if the logged in user is a member of the actAs group, then allow
            // the actAs
            if (actAsGroup.hasMember(loggedInSubject)) {
              //its ok, lets add to cache
              actAsCache().put(cacheKey, Boolean.TRUE);
              // this is the subject the web service wants to use
              return actAsSubject;
            }
          }
          countNoExceptions++;
        } catch (Exception e) {
          //just log and dont act since other entries could be fine
          LOG.error("Problem with groupEntry: " + groupEntry + ", loggedInUser: "
              + loggedInSubject + ", actAsSubject: " + actAsSubject, e);
        }

      }

      if (countNoExceptions == 0) {
        throw new RuntimeException("Problems seeing if web service user '"
            + loggedInSubject + "' can actAs the other subject: '" + actAsSubject + "'");
      }
      // if not an effective member
      throw new RuntimeException(
          "A web service is specifying an actAsUser, but the groups specified in "
              + GrouperWsConfig.WS_ACT_AS_GROUP + " in the grouper-ws.properties "
              + " does not have a valid rule for member: '" + loggedInSubject
              + "', and actAs: '" + actAsSubject + "'");
    } catch (SessionException se) {
      throw new RuntimeException(se);
    } finally {
      GrouperSession.stopQuietly(session);
    }

  }

  /**
   * 
   */
  private static final long serialVersionUID = 1L;

  /**
   * thread local for servlet
   */
  private static ThreadLocal threadLocalServlet = new ThreadLocal();

  /**
   * thread local for request
   */
  private static ThreadLocal threadLocalRequest = new ThreadLocal();

  /**
   * thread local for request
   */
  private static ThreadLocal threadLocalRequestStartMillis = new ThreadLocal();

  /**
   * thread local for response
   */
  private static ThreadLocal threadLocalResponse = new ThreadLocal();

  /**
   * public method to get the http servlet request
   * 
   * @return the http servlet request
   */
  public static HttpServletRequest retrieveHttpServletRequest() {
    return threadLocalRequest.get();
  }

  /**
   * public method to get the http servlet
   * 
   * @return the http servlet
   */
  public static HttpServlet retrieveHttpServlet() {
    return threadLocalServlet.get();
  }

  /**
   * is this a wssec servlet?  must have servlet init param
   * @return true if wssec
   */
  public static boolean wssecServlet() {
    String wssecValue = retrieveHttpServlet().getServletConfig()
        .getInitParameter("wssec");
    return GrouperUtil.booleanValue(wssecValue, false);
  }

  /**
   * public method to get the http servlet
   * 
   * @param httpServlet is servlet to assign
   */
  public static void assignHttpServlet(HttpServlet httpServlet) {
    threadLocalServlet.set(httpServlet);
  }

  /**
   * public method to get the http servlet request
   * 
   * @return the http servlet request
   */
  public static HttpServletResponse retrieveHttpServletResponse() {
    return threadLocalResponse.get();
  }

  /**
   * filter method
   */
  public void destroy() {
    InstrumentationThread.shutdownThread();
  }

  /**
   * 
   * @return the debug map
   */
  public static Map retrieveDebugMap() {
    
    HttpServletRequest httpServletRequest = retrieveHttpServletRequest();
    
    if (httpServletRequest == null) {
      //dont want a null pointer exception
      //wont get logged anyways
      return new LinkedHashMap();
    }
    
    Map debugMap = (Map)httpServletRequest.getAttribute("debugMap");

    if (debugMap == null) {
      debugMap = new LinkedHashMap();
      httpServletRequest.setAttribute("debugMap", debugMap);
    }
    
    return debugMap;

  }
  
  /**
   * time format in logs
   */
  private static SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
  
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain filterChain) throws IOException, ServletException {

    Map debugMap = new LinkedHashMap();
    Long start = System.nanoTime();
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    
    String xRequestId = httpServletRequest.getHeader("X-Request-Id");
    if (!StringUtils.isBlank(xRequestId)) {
      GrouperLoggerState grouperLoggerState = GrouperLogger.retrieveGrouperLoggerState(true);
      grouperLoggerState.setRequestId(xRequestId);
    }

    String xCorrelationId = httpServletRequest.getHeader("X-Correlation-Id");
    if (!StringUtils.isBlank(xCorrelationId)) {
      GrouperLoggerState grouperLoggerState = GrouperLogger.retrieveGrouperLoggerState(true);
      grouperLoggerState.setCorrelationId(xCorrelationId);
    }
    try {

      request.setCharacterEncoding("UTF-8");
      response.setCharacterEncoding("UTF-8");

      //make sure nulls are not returned for params for Axis bug where
      //empty strings work, but nulls make things off a bit
      request = new WsHttpServletRequest((HttpServletRequest) request);
      
      String authHeader = ((HttpServletRequest) request).getHeader("Authorization");
      if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith("Bearer jwtTrusted_")) {
        Subject subject = new GrouperTrustedJwt().assignBearerTokenHeader(authHeader).decode();
        if (subject != null) {
          ((HttpServletRequest) request).setAttribute("REMOTE_USER", subject.getSourceId()+"::::"+subject.getId());
        } else {
          ((HttpServletResponse) response).sendError(401, "Unauthorized");          
          return;
        }
      } else if (StringUtils.isNotBlank(authHeader) && (authHeader.startsWith("Bearer oidc_") || authHeader.startsWith("Bearer oidcWithRedirectUri_"))) {
        
        Subject subject = new GrouperOidc().assignBearerTokenHeader(authHeader)
            .assignWs(true)
            .decode();
        if (subject != null) {
          ((HttpServletRequest) request).setAttribute("REMOTE_USER", subject.getSourceId()+"::::"+subject.getId());
        } else {
          ((HttpServletResponse) response).sendError(401, "Unauthorized");          
          return;
        }
        
      } else if (StringUtils.isNotBlank(authHeader) && authHeader.startsWith("Bearer jwtUser_")) {
        
        Subject subject = new GrouperPublicPrivateKeyJwt().assignBearerTokenHeader(authHeader).decode(request.getRemoteAddr());
        
        if (subject != null) {
          ((HttpServletRequest) request).setAttribute("REMOTE_USER", subject.getSourceId()+"::::"+subject.getId());
        } else {
          ((HttpServletResponse) response).sendError(401, "Unauthorized");          
          return;
        }
        
      } else {
        
        boolean runGrouperWsWithBasicAuth = GrouperHibernateConfig.retrieveConfig().propertyValueBoolean("grouper.is.ws.basicAuthn", false);
        if (runGrouperWsWithBasicAuth) {
          // String authHeader = ((HttpServletRequest) request).getHeader("Authorization");
          
          boolean isValid = new Authentication().authenticate(authHeader, GrouperPassword.Application.WS, request.getRemoteAddr());
          
          if (!isValid) {
            ((HttpServletResponse) response).setHeader("WWW-Authenticate", "Basic realm=\"" + "Protected" + "\"");
            ((HttpServletResponse) response).sendError(401, "Unauthorized");          
            return;
          } else {
            String userName = Authentication.retrieveUsername(authHeader);
            ((HttpServletRequest) request).setAttribute("REMOTE_USER", userName);
          }
          
        } else {
          // also return 401 if authentication fails and using custom authentication, not sure about the rampart case
          String authenticationClassName = GrouperWsConfig.retrieveConfig().propertyValueString(GrouperWsConfig.WS_SECURITY_NON_RAMPART_AUTHENTICATION_CLASS, null);
          boolean checkAuthentication = GrouperWsConfig.retrieveConfig().propertyValueBoolean("ws.security.non-rampart.error.401.authentication.error", true);
          
          if (!StringUtils.isEmpty(authenticationClassName) && checkAuthentication) {
            Class theClass = GrouperUtil.forName(authenticationClassName);

            WsCustomAuthentication wsAuthentication = GrouperUtil.newInstance(theClass);

            String userIdLoggedIn = wsAuthentication.retrieveLoggedInSubjectId((HttpServletRequest) request);
            if (StringUtils.isBlank(userIdLoggedIn)) {
              ((HttpServletResponse) response).setHeader("WWW-Authenticate", "Basic realm=\"" + "Protected" + "\"");
              ((HttpServletResponse) response).sendError(401, "Unauthorized");          
              return;
            }
          }
        }
        
      }
      
      request.setAttribute("debugMap", debugMap);

      ThreadContext.clearStack();
      
      //servlet will set this...
      threadLocalServlet.remove();
      threadLocalRequest.set((HttpServletRequest) request);
      
      debugMap = retrieveDebugMap();
      
      threadLocalResponse.set((HttpServletResponse) response);
      threadLocalRequestStartMillis.set(System.currentTimeMillis());
      
      GrouperContextTypeBuiltIn.setDefaultContext(GrouperContextTypeBuiltIn.GROUPER_WS);
  
      //lets add the request, session, and response
      HooksContext.setAttributeThreadLocal(HooksContext.KEY_HTTP_SERVLET_REQUEST, request, false);
      HooksContext.setAttributeThreadLocal(HooksContext.KEY_HTTP_SESSION, 
          ((HttpServletRequest)request).getSession(), false);
      HooksContext.setAttributeThreadLocal(HooksContext.KEY_HTTP_SERVLET_RESPONSE, response, false);
  
      GrouperContext grouperContext = GrouperContext.createNewDefaultContext(
          GrouperEngineBuiltin.WS, false, false);
      
      String xForwardedFor = ((HttpServletRequest)request).getHeader("X-Forwarded-For");
      String remoteAddr = StringUtils.defaultIfBlank(xForwardedFor, request.getRemoteAddr());
      grouperContext.setCallerIpAddress(remoteAddr);
      
      //get the proxy IP address
      debugMap.put("start", timeFormat.format(new Date()));
      debugMap.put("remoteAddr", remoteAddr);
      debugMap.put("requestUrl", ((HttpServletRequest)request).getRequestURL());
      
      filterChain.doFilter(request, response);

    } catch (ServletException se) {
      handleException(se);
      throw se;
    } catch (IOException ioe) {
      handleException(ioe);
      throw ioe;
    } catch (RuntimeException re) {
      LOG.info("error in request", re);
      debugMap.put("exception", ExceptionUtils.getFullStackTrace(re));
      handleException(re);
      throw re;
    } finally {
      
      GrouperWsLog.wsLog(debugMap, start);
      GrouperWsLongRunningLog.wsLog(debugMap, start);
      
      GrouperLogger.clearGrouperLoggerState();

      threadLocalRequest.remove();
      threadLocalResponse.remove();
      threadLocalRequestStartMillis.remove();
      threadLocalServlet.remove();
      
      HooksContext.clearThreadLocal();
      ServletRequestUtils.requestEnd();

    }

  }

  /**
   * 
   * @param se
   * @throws ServletException
   */
  private void handleException(IOException se) throws IOException {
    try {
      handleExceptionHelper(se);
    } catch (Throwable t) {
      throw (IOException) se;
    }
  }


  
  /**
   * 
   * @param se
   * @throws ServletException
   */
  private void handleException(ServletException se) throws ServletException {
    try {
      handleExceptionHelper(se);
    } catch (Throwable t) {
      throw (ServletException) se;
    }
  }

  /**
   * 
   * @param se
   * @throws ServletException
   */
  private void handleException(RuntimeException se) throws ServletException {
    try {
      handleExceptionHelper(se);
    } catch (Throwable t) {
      throw (RuntimeException) se;
    }
  }

  /**
   * dont throw simple exception if configured not to throw detailed exception
   * this method will throw the exception
   * @param se
   */
  private void handleExceptionHelper(Throwable se) throws Throwable {

    LOG.error("Error processing request", se);
    if (GrouperWsConfig.retrieveConfig().propertyValueBoolean("ws.throwExceptionsToClient", true)) {
      throw new RuntimeException("Error processing request, check logs", se);
    }
  }

  /**
   * filter method
   */
  public void init(FilterConfig arg0) throws ServletException {
    
    GrouperContext.createNewDefaultContext(GrouperEngineBuiltin.WS, false, false);

    GrouperStartup.startup();
    GrouperStartup.waitForGrouperStartup();

    InstrumentationThread.startThread(GrouperEngineBuiltin.WS, null);
    
    
    initOnce();
  }

  /**
   * mark group as cacheable
   * should we auto-create?
   * @param name
   */
  private static void initGroup(final String name) {
    if (StringUtils.isBlank(name)) {
      return;
    }
    
    GrouperSession.internal_callbackRootGrouperSession(new GrouperSessionHandler() {
      
      public Object callback(GrouperSession grouperSession) throws GrouperSessionException {
        
        Group group = GroupFinder.findByName(grouperSession, name, false);
        if (group != null) {
          
          GroupFinder.groupCacheAsRootAddSystemGroup(group);
          
        }
        
        return null;
      }
    });
  }

  /**
   * if initted
   */
  private static boolean inittedOnce = false;

  /**
   * 
   */
  private static void initOnce() {
    
    if (!inittedOnce) {
      synchronized (GrouperServiceJ2ee.class) {
        if (!inittedOnce) {

          //  # Web service users who are in the following group can use the actAs field to act as someone else
          //  # You can put multiple groups separated by commas.  e.g. a:b:c, e:f:g
          //  # You can put a single entry as the group the calling user has to be in, and the grouper the actAs has to be in
          //  # separated by 4 colons
          //  # e.g. if the configured values is:       a:b:c, e:f:d :::: r:e:w, x:e:w
          //  # then if the calling user is in a:b:c or x:e:w, then the actAs can be anyone
          //  # if not, then if the calling user is in e:f:d, then the actAs must be in r:e:w.  If multiple rules, then 
          //  # if one passes, then it is a success, if they all fail, then fail.
          //  ws.act.as.group = etc:webServiceActAsGroup
          //
          //  # similar syntax as ws.act.as.group but for the grouper actas (e.g. for grouper messaging to WS bridge)
          //  ws.grouper.act.as.group = 
          
          String debugGroupConfig = GrouperWsConfig.retrieveConfig().propertyValueString("browser.debug.group");
          String grouperDebugGroupConfig = GrouperWsConfig.retrieveConfig().propertyValueString("ws.grouper.act.as.group");
          
          for (String config : new String[]{debugGroupConfig, grouperDebugGroupConfig}) {
            if (!StringUtils.isBlank(config)) {
              for (String groupNameConfig : GrouperUtil.splitTrim(config, ",")) {
                //now see if it is a multi input
                if (StringUtils.contains(groupNameConfig, GrouperWsConfig.WS_SEPARATOR)) {
  
                  //it is the group the user is in, and the group the act as has to be in
                  String[] groupEntryArray = GrouperUtil.splitTrim(groupNameConfig,
                      GrouperWsConfig.WS_SEPARATOR);
                  initGroup(groupEntryArray[0]);
                  initGroup(groupEntryArray[1]);
                } else {
                  initGroup(groupNameConfig);
                }
              }
            }
          }
          
          //  # If there is an entry here for group name, then all web service client users must be in this group (before the actAs)
          //  #ws.client.user.group.name = etc:webServiceClientUsers
          initGroup(GrouperWsConfig.retrieveConfig().propertyValueString("ws.client.user.group.name"));
          
          inittedOnce = true;
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy