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

edu.internet2.middleware.subject.provider.LdapSourceAdapter Maven / Gradle / Ivy

There is a newer version: 5.13.5
Show newest version
/**
 * Copyright 2014 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.
 */
/* ========================================================================
 * Copyright (c) 2009-2011 The University of Washington
 *
 * 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.
 * ========================================================================
 */

/*
 * Ldap subject adapter
 * @author fox
 */

package edu.internet2.middleware.subject.provider;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.internet2.middleware.grouper.ldap.LdapAttribute;
import edu.internet2.middleware.grouper.ldap.LdapEntry;
import edu.internet2.middleware.grouper.ldap.LdapSearchScope;
import edu.internet2.middleware.grouper.ldap.LdapSessionUtils;
import edu.internet2.middleware.grouper.util.GrouperUtil;
import edu.internet2.middleware.grouperClient.util.GrouperClientUtils;
import edu.internet2.middleware.subject.SearchPageResult;
import edu.internet2.middleware.subject.SourceUnavailableException;
import edu.internet2.middleware.subject.Subject;
import edu.internet2.middleware.subject.SubjectCaseInsensitiveMapImpl;
import edu.internet2.middleware.subject.SubjectNotFoundException;
import edu.internet2.middleware.subject.SubjectNotUniqueException;
import edu.internet2.middleware.subject.SubjectTooManyResults;
import edu.internet2.middleware.subject.SubjectUtils;

/**
 * Ldap source adapter.  
 */

public class LdapSourceAdapter extends BaseSourceAdapter {


  @Override
  public void loggingStart() {
    LdapSessionUtils.logStart();
  }

  @Override
  public String loggingStop() {
    return LdapSessionUtils.logEnd();
  }

  private static Log log = edu.internet2.middleware.grouper.util.GrouperUtil.getLog(LdapSourceAdapter.class);

  private Properties props;
  protected String subjectIDAttributeName = null;
  protected boolean subjectIDFormatToLowerCase = false;
  private String ldapServerId = null;

  private boolean multipleResults = false;

  private String[] allAttributeNames;

  private boolean throwErrorOnFindAllFailure;

  /** if there is a limit to the number of results */
  private Integer maxResults;
  
  /** if error if limit is reached */
  private boolean errorOnMaxResults = true;
  
  /** if there is a limit to the number of results */
  private Integer maxPage;

  /**
   * 
   */
  public LdapSourceAdapter() {
    super();
  }

  /**
   * @param id
   * @param name
   */
  public LdapSourceAdapter(String id, String name) {
    super(id, name);
  }

  /**
   * {@inheritDoc}
   */
  public void init() {
    log.debug("ldap source init");
    props = initParams();

    ldapServerId = getNeededProperty(props, "ldapServerId");
    nameAttributeName = getNeededProperty(props,"Name_AttributeType");
    subjectIDAttributeName = getNeededProperty(props,"SubjectID_AttributeType");
    descriptionAttributeName = getNeededProperty(props,"Description_AttributeType");
    
    
    // if this is the new source config then dereference the subject attributes with the source
    // columns or ldap attributes
    subjectIDAttributeName = convertSubjectAttributeToSourceAttribute(subjectIDAttributeName);
    descriptionAttributeName = convertSubjectAttributeToSourceAttribute(descriptionAttributeName);
    nameAttributeName = convertSubjectAttributeToSourceAttribute(nameAttributeName);
    

    subjectIDFormatToLowerCase = SubjectUtils.booleanValue(props.getProperty("SubjectID_formatToLowerCase"), false);

    String mr = props.getProperty("Multiple_Results");
    if (mr!=null && (mr.equalsIgnoreCase("yes")||mr.equalsIgnoreCase("true"))) multipleResults = true;

    Set attributeNameSet = this.getAttributes();
    
    if (!this.isEditable()) {
      allAttributeNames = new String[3+attributeNameSet.size()];
      allAttributeNames[0] = nameAttributeName;
      allAttributeNames[1] = subjectIDAttributeName;
      allAttributeNames[2] = descriptionAttributeName;
      int i = 0;
      for (Iterator it = attributeNameSet.iterator(); it.hasNext(); allAttributeNames[3+i++]= (String) it.next());
    } else {
      allAttributeNames = GrouperUtil.toArray(attributeNameSet, String.class);
    }
    

    Map virtualAttributes = SubjectUtils.nonNull(BaseSourceAdapter.virtualAttributesForSourceLegacy(this));

    // GRP-1669: grouper sends virtual attribute names to ldap
    //take out dupes and virtuals
    Set attributeSet = SubjectUtils.toSet(allAttributeNames);
    attributeSet.removeAll(virtualAttributes.keySet());
    allAttributeNames = toArray(attributeSet, String.class);

    String throwErrorOnFindAllFailureString = this.getInitParam("throwErrorOnFindAllFailure");
    throwErrorOnFindAllFailure = SubjectUtils.booleanValue(throwErrorOnFindAllFailureString, true);


    {
      String maxPageString = props.getProperty("maxPageSize");
      if (!StringUtils.isBlank(maxPageString)) {
        try {
          this.maxPage = Integer.parseInt(maxPageString);
        } catch (NumberFormatException nfe) {
          throw new SourceUnavailableException("Cant parse maxPage: " + maxPageString, nfe);
        }
      }
    }

    {
      String maxResultsString = props.getProperty("maxResults");
      if (!StringUtils.isBlank(maxResultsString)) {
        try {
          this.maxResults = Integer.parseInt(maxResultsString);
        } catch (NumberFormatException nfe) {
          throw new SourceUnavailableException("Cant parse maxResults: " + maxResultsString, nfe);
        }
      }
    }
    
    {
      String errorOnMaxResultsString = props.getProperty("errorOnMaxResults");
      if (!StringUtils.isBlank(errorOnMaxResultsString)) {
        this.errorOnMaxResults = SubjectUtils.booleanValue(errorOnMaxResultsString, true);
      }
    }
  }

  /**
   * convert a list into an array of type of theClass
   * @param  is the type of the array
   * @param collection list to convert
   * @param theClass type of array to return
   * @return array of type theClass[] filled with the objects from list
   */
  @SuppressWarnings("unchecked")
  public static  T[] toArray(Collection collection, Class theClass) {
    if (collection == null || collection.size() == 0) {
      return null;
    }

    return (T[])collection.toArray((Object[]) Array.newInstance(theClass,
        collection.size()));

  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Subject getSubject(String id, boolean exceptionIfNull)
      throws SubjectNotFoundException,SubjectNotUniqueException {
    Subject subject = null;
    Search search = getSearch("searchSubject");
    if (search == null) {
      log.error("searchType: \"searchSubject\" not defined.");
      return subject;
    }
    try {
      LdapEntry entry = getLdapUnique( search, id, allAttributeNames   );
      subject = createSubject(entry);
      if (subject == null && exceptionIfNull) {
        throw new SubjectNotFoundException("Subject " + id + " not found.");
      }
    } catch (SubjectNotFoundException e) {
      if (exceptionIfNull) throw e;
    }
    return subject;
  }

  /**
   * {@inheritDoc}
   * @deprecated
   */
  @Deprecated
  @Override
  public Subject getSubject(String id) throws SubjectNotFoundException, SubjectNotUniqueException {
    return this.getSubject(id, true); 
  }


  /**
   * {@inheritDoc}
   */
  @Override
  public Subject getSubjectByIdentifier(String id, boolean exceptionIfNull)
      throws SubjectNotFoundException, SubjectNotUniqueException {
    Map debugLog = null;
    try {
      if (log.isDebugEnabled()) {
        debugLog = new LinkedHashMap();
        debugLog.put("method", "getSubjectByIdentifier");
        debugLog.put("id", id);
        debugLog.put("exceptionIfNull", exceptionIfNull);
      }
      Subject subject = null;
      Search search = getSearch("searchSubjectByIdentifier");
      if (debugLog != null) {
        debugLog.put("search", search);
      }

      if (search == null) {
        log.error("searchType: \"searchSubjectByIdentifier\" not defined.");
        return subject;
      }
      try {
        LdapEntry entry = getLdapUnique(search, id, allAttributeNames);
        subject = createSubject(entry);
        if (debugLog != null) {
          debugLog.put("foundSubject", subject != null);
        }
        if (subject == null && exceptionIfNull) {
          throw new SubjectNotFoundException("Subject " + id + " not found.");
        }
      } catch (SubjectNotFoundException e) {
        if (exceptionIfNull) {
          throw e;
        }
        return null;
      }
      search = getSearch("searchSubjectByIdentifierAttributes");
      if (debugLog != null) {
        debugLog.put("searchSubjectByIdentifierAttributesNotNull", search!=null);
      }
      return subject;

    } finally {
      if (log.isDebugEnabled()) {
        log.debug(SubjectUtils.mapToString(debugLog));
      }
    }

  }

  /**
   * {@inheritDoc}
   * @deprecated
   */
  @Deprecated
  @Override
  public Subject getSubjectByIdentifier(String id) throws SubjectNotFoundException, SubjectNotUniqueException {
    return this.getSubjectByIdentifier(id, true);
  } 

  /**
   * @see edu.internet2.middleware.subject.provider.BaseSourceAdapter#searchPage(java.lang.String)
   */
  @Override
  public SearchPageResult searchPage(String searchValue) {
    return searchHelper(searchValue, true);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public Set search(String searchValue) {
    return searchHelper(searchValue, false).getResults();
  }

  /**
   * @param searchValue 
   * @return the set
   */
  private SearchPageResult searchHelper(String searchValue, boolean firstPageOnly) {

    boolean tooManyResults = false;
    Comparator cp = new LdapComparator();
    TreeSet result = new TreeSet(cp);
    Search search = getSearch("search");
    if (search == null) {
      log.error("searchType: \"search\" not defined.");
      return new SearchPageResult(tooManyResults, result);
    }
    Search searchA = getSearch("searchAttributes");
    boolean noAttrSearch = true;
    if (searchA!=null) noAttrSearch = false;

    try {
      Iterator ldapResults = getLdapResultsHelper(search,searchValue, allAttributeNames, firstPageOnly);
      if (ldapResults == null) {
        return new SearchPageResult(tooManyResults, result);
      }
      
      while ( ldapResults.hasNext()) {        
        //if we are at the end of the page
        if (firstPageOnly && this.maxPage != null && result.size() >= this.maxPage) {
          tooManyResults = true;
          break;
        }
        
        if (this.maxResults != null && result.size() >= this.maxResults) {
          if (!errorOnMaxResults) {
            break;
          }
          
          throw new SubjectTooManyResults(
              "More results than allowed: " + this.maxResults
              + " for search '" + searchValue + "'");
        }

        LdapEntry si = ldapResults.next();
        Subject subject = createSubject(si);

        if (subject != null) {
          result.add(subject);
        } else {
          log.error("Failed to create subject with attributes: " + si.toString());  
        }


      }

      if (log.isDebugEnabled()) {
        log.debug("set has " + result.size() + " subjects");
        if (result.size()>0) log.debug("first is " + ((Subject)result.first()).getName());
      }

    } catch (Exception ex) {

      if (ex instanceof SubjectTooManyResults) {
        throw (SubjectTooManyResults)ex;
      }
      
      if (throwErrorOnFindAllFailure) {
        throw new SourceUnavailableException(ex.getMessage() + ", source: " + this.getId() + ", sql: " + search.getParam("sql"), ex);
      }

      log.error("LDAP Naming Except: " + ex.getMessage() + ", " + this.id + ", " + searchValue, ex);
    }

    return new SearchPageResult(tooManyResults, result);
  }

  /**
   * @param attributes
   */
  public Subject createSubject(LdapEntry entry) {
    String subjectID = "";

    if (entry==null) {
      log.error("Ldap createSubject called with null entry.");
      return (null);
    }
    LdapAttribute attribute = entry.getAttribute(subjectIDAttributeName);
    if (attribute == null || attribute.getStringValues().size() == 0) {
      log.error("No value for LDAP attribute \"" + subjectIDAttributeName + "\". It is Grouper attribute \"SubjectID\".\".  Subject's problematic attributes : " + entry.toString());
      return null;
    }
    subjectID   = attribute.getStringValues().iterator().next();
    if (this.subjectIDFormatToLowerCase) {
      subjectID = subjectID.toLowerCase();
    }

    SubjectImpl subject = new SubjectImpl(subjectID, null, null, this.getSubjectType().getName(), this.getId(), nameAttributeName, descriptionAttributeName);

    // add the attributes

    Map> myAttributes = new  SubjectCaseInsensitiveMapImpl>();
    for (Iterator e = entry.getAttributes().iterator(); e.hasNext();) {
      LdapAttribute attr = e.next();
      String attrName = attr.getName();
      
      attrName = convertSourceAttributeToSubjectAttribute(attrName);

      Set values = new HashSet();
      values.addAll(attr.getStringValues());
      myAttributes.put(attrName, values);
    }
    subject.setAttributes(myAttributes);

    return subject;
  }

  protected String getNeededProperty(Properties props, String prop) {
    String value = props.getProperty(prop);
    if (value==null) {
      log.error("Property '" + prop + "' is not defined!");
    }
    return (value);
  }


  /**
   * Try to get more attributes for the subject.
   * @param subject
   */
  protected Map> getAllAttributes(SubjectImpl subject) {
    Map> attributes = new  SubjectCaseInsensitiveMapImpl>();
    if (log.isDebugEnabled()) {
      log.debug("getAllAttributes for " + subject.getName());
    }
    Search search = getSearch("searchSubjectAttributes");
    if (search == null) {
      log.debug("searchType: \"searchSubjectAttributes\" not defined.");
      return attributes;
    }

    try {
      LdapEntry entry = getLdapUnique(search,subject.getName(), allAttributeNames);
      for (Iterator e = entry.getAttributes().iterator(); e.hasNext();) {
        LdapAttribute attr = e.next();
        String attrName = attr.getName();

        Set values = new HashSet();
        values.addAll(attr.getStringValues());
        attributes.put(attrName, values);
      }
      subject.setAttributes(attributes);
    } catch (SubjectNotFoundException ex ) {
      log.error("SubjectNotFound: "+ subject.getId() +" " + ex.getMessage(), ex);
    } catch (SubjectNotUniqueException ex ) {
      log.error("SubjectNotUnique: "+ subject.getId() +" " + ex.getMessage(), ex);
    }
    return attributes;
  }

  protected Iterator getLdapResults(Search search, String searchValue, String[] attributeNames) {
    return getLdapResultsHelper(search, searchValue, attributeNames, false); 
  }

  private Iterator getLdapResultsHelper(Search search, String searchValue, String[] attributeNames, boolean firstPageOnly ) {
    if (searchValue == null ) {
      return null;
    }

    SubjectStatusResult subjectStatusResult = null;

    //if this is a search and not by id or identifier
    boolean subjectStatusQuery = StringUtils.equals("search", search.getSearchType());
    if (subjectStatusQuery) {
      //see if we are doing status
      SubjectStatusProcessor subjectStatusProcessor = new SubjectStatusProcessor(searchValue, this.getSubjectStatusConfig());
      subjectStatusResult = subjectStatusProcessor.processSearch();

      //strip out status parts
      searchValue = subjectStatusResult.getStrippedQuery();
    }      

    String filter = null;
    Iterator results = null;
    int cp;
    String aff = null;

    // build a filter
    if ((cp=searchValue.indexOf(',')) >0 ) {
      int lb, rb;
      if ( (lb=searchValue.indexOf('['))>cp && (rb=searchValue.indexOf(']'))>lb ) {
        aff = searchValue.substring(lb+1, rb);
        searchValue = searchValue.substring(0, lb);
        // log.debug("first, last [" + aff + "] search: " + searchValue);
        filter = search.getParam("affiliationfilter");
      } else {
        // log.debug("first, last search: " + searchValue);
        filter = search.getParam("firstlastfilter");
      }
      if (filter==null) filter = search.getParam("filter");  // fall back
      if (filter==null) {
        log.error("Search filter not found for search type:  " + search.getSearchType());
        return null;
      }
      String last = searchValue.substring(0, cp);
      String first = searchValue.substring(cp+1);
      if (last!=null) filter = GrouperClientUtils.replace(filter, "%LAST%", escapeSearchFilter(last));
      if (first!=null) filter = GrouperClientUtils.replace(filter, "%FIRST%", escapeSearchFilter(first));
      if (aff!=null) filter = GrouperClientUtils.replace(filter, "%AFFILIATION%", escapeSearchFilter(aff));
    } else {
      // simple search
      filter = search.getParam("filter");
      if (filter==null) {
        log.error("Search filter not found for search type:  " + search.getSearchType());
        return results;
      }
      filter = GrouperClientUtils.replace(filter, "%TERM%", escapeSearchFilter(searchValue));
    }

    String preStatusFilter = filter;
    if (subjectStatusQuery && !subjectStatusResult.isAll() && !StringUtils.isBlank(subjectStatusResult.getDatastoreFieldName())) {

      //validate the status value
      if (!subjectStatusResult.getDatastoreValue().matches("[a-zA-Z0-9_-]+")) {
        throw new RuntimeException("Invalid status value: " + subjectStatusResult.getDatastoreValue());
      }

      //wrap the query in a status part
      filter = "(&" + filter + "(" + (subjectStatusResult.isEquals()?"":" ! ( ") + subjectStatusResult.getDatastoreFieldName() + "=" 
          + subjectStatusResult.getDatastoreValue() + (subjectStatusResult.isEquals()?"":" ) ") + "))";

    }

    if (!StringUtils.equals(preStatusFilter, filter)) {
      if (log.isDebugEnabled()) {
        log.debug("searchType: " + search.getSearchType() + ", preStatusFilter: " + preStatusFilter + ", filter: " + filter);
      }

    } else {
      if (log.isDebugEnabled()) {
        log.debug("searchType: " + search.getSearchType() + ", filter: " + filter);
      }

    }

    try  {

      Integer resultSetLimit = resultSetLimit(firstPageOnly, this.getMaxPage(), this.maxResults);
      Integer sizeLimit = null;
      if (resultSetLimit != null) {
        sizeLimit = resultSetLimit + 1;
      }

      // get params
      String base = search.getParam("base");

      String searchScopeString = search.getParam("scope");   
      LdapSearchScope searchScope = null;
      
      if (searchScopeString != null) {
        searchScope = LdapSearchScope.valueOf(searchScopeString);
      }

      if (StringUtils.isEmpty(ldapServerId)) {
        throw new SourceUnavailableException("Missing property subjectApi.source." + this.id + ".param.ldapServerId.value in subject.properties.  "
            + "The value of this property should point to your LDAP configuration in the grouper-loader.properties file. "
            + "e.g. if your grouper-loader.properties has ldap.personLdap.url, then the value of this property would be personLdap.");
      }
      
      results = LdapSessionUtils.ldapSession().list(ldapServerId, base, searchScope, filter, attributeNames, sizeLimit).iterator();
    } catch (Exception ex) {  // don't know if there are others
      log.error("Ldap Exception: " + ex.getMessage(), ex);
      throw new SourceUnavailableException("Ldap Exception: " + ex.getMessage(), ex);
    }
    
    return results;
  }


  protected LdapEntry getLdapUnique( Search search, String searchValue, String[] attributeNames)
      throws SubjectNotFoundException,SubjectNotUniqueException, SourceUnavailableException  {


    Map debugLog = null;
    try {
      if (log.isDebugEnabled()) {
        debugLog = new LinkedHashMap();
        debugLog.put("method", "getLdapUnique");
        debugLog.put("search", search);
        debugLog.put("searchValue", searchValue);
        debugLog.put("attributeNames", SubjectUtils.toStringForLog(attributeNames, 200));
      }

      Iterator results = getLdapResults(search, searchValue, attributeNames);

      if (results == null || !results.hasNext()) {
        String errMsg = "No results: " + search.getSearchType() + " filter:" + search.getParam("filter") + " searchValue: " + searchValue;
        throw new SubjectNotFoundException( errMsg);
      }

      LdapEntry overall = results.next();

      if (debugLog!=null) {
        debugLog.put("dn", overall.getDn());
      }

      // Add the DN to the returned attributes.
      LdapAttribute dnAttribute = new LdapAttribute("dn");
      dnAttribute.addStringValue(overall.getDn());
      overall.addAttribute(dnAttribute);

      if (results.hasNext()) {
        LdapEntry si = results.next();
        if (debugLog!=null) {
          debugLog.put("dn2", si.getDn());
        }
        if (!multipleResults) {
          if (debugLog!=null) {
            debugLog.put("searchIsNotUnique", true);
          }
          String errMsg ="Search is not unique:" + si.getDn() + "\n";
          throw new SubjectNotUniqueException( errMsg );
        }
        Iterator n = si.getAttributes().iterator();
          while (n.hasNext()) {
            LdapAttribute a = n.next();
            if (log.isDebugEnabled()) {
              log.debug("checking attribute " + a.getName());
            }
            if (overall.getAttribute(a.getName())==null || 
                (overall.getAttribute(a.getName()).getStringValues().size() == 0 && overall.getAttribute(a.getName()).getBinaryValues().size() == 0)) {
              if (log.isDebugEnabled()) {
                log.debug("adding " + a.getName());
              }
              overall.addAttribute(a);
            }
          }

        // Add the DN to the returned attributes.
        overall.getAttribute("dn").addStringValue(si.getDn());
      }
      return overall;

    } finally {
      if (log.isDebugEnabled()) {
        log.debug(SubjectUtils.mapToString(debugLog));
      }
    }


  }

  /**
   * Escape a search filter to prevent LDAP injection.
   * From http://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
   * 
   * @param filter
   * @return escaped filter
   */
  protected String escapeSearchFilter(String filter) {
    //From RFC 2254
    String escapedStr = new String(filter);
    // note, these are regexes, so thats why there is an extra slash
    escapedStr = escapedStr.replaceAll("\\\\","\\\\5c");
    // We want people to be able to use wildcards.
    // escapedStr = escapedStr.replaceAll("\\*","\\\\2a");
    escapedStr = escapedStr.replaceAll("\\(","\\\\28");
    escapedStr = escapedStr.replaceAll("\\)","\\\\29");
    escapedStr = escapedStr.replaceAll("\\"+Character.toString('\u0000'), "\\\\00");
    return escapedStr;
  }
  /**
   * @see edu.internet2.middleware.subject.Source#checkConfig()
   */
  public void checkConfig() {
  }
  /**
   * @see edu.internet2.middleware.subject.Source#printConfig()
   */
  public String printConfig() {
    StringBuilder message = new StringBuilder("subject.properties ldap source id:   ").append(this.getId()).append(": ");
    message.append(ldapServerId);

    return message.toString();
  }

  /**
   * Set whether or not multiple results are allowed. Primarily for tests.
   * 
   * @param multipleResults
   */
  public void setMultipleResults(boolean multipleResults) {
    this.multipleResults = multipleResults;
  }

  /**
   * max Page size
   * @return the maxPage
   */
  public Integer getMaxPage() {
    return this.maxPage;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy