/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS * Portions Copyright 2013 Manuel Gaupp */ package org.opends.server.authorization.dseecompat; import java.util.LinkedList; import java.util.List; import java.util.SortedSet; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.opends.server.admin.std.server.DseeCompatAccessControlHandlerCfg; import org.opends.server.api.AccessControlHandler; import org.opends.server.api.ClientConnection; import org.opends.server.api.ConfigHandler; import org.opends.server.backends.pluggable.SuffixContainer; import org.opends.server.controls.GetEffectiveRightsRequestControl; import org.opends.server.core.BindOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ExtendedOperation; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.SearchOperation; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.protocols.internal.SearchRequest; import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.types.*; import org.opends.server.workflowelement.localbackend.*; import static org.opends.messages.AccessControlMessages.*; import static org.opends.server.authorization.dseecompat.Aci.*; import static org.opends.server.authorization.dseecompat.EnumEvalReason.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.protocols.internal.Requests.*; import static org.opends.server.schema.SchemaConstants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * The AciHandler class performs the main processing for the dseecompat package. */ public final class AciHandler extends AccessControlHandler<DseeCompatAccessControlHandlerCfg> { /** * String used to indicate that the evaluating ACI had a all * operational attributes targetattr match (targetattr="+"). */ public static final String ALL_OP_ATTRS_MATCHED = "allOpAttrsMatched"; /** * String used to indicate that the evaluating ACI had a all user * attributes targetattr match (targetattr="*"). */ public static final String ALL_USER_ATTRS_MATCHED = "allUserAttrsMatched"; /** * String used to save the original authorization entry in an * operation attachment if a proxied authorization control was seen. */ public static final String ORIG_AUTH_ENTRY = "origAuthorizationEntry"; /** Attribute type corresponding to "aci" attribute. */ static AttributeType aciType; /** Attribute type corresponding to global "ds-cfg-global-aci" attribute. */ static AttributeType globalAciType; /** Attribute type corresponding to "debugsearchindex" attribute. */ private static AttributeType debugSearchIndex; /** DN corresponding to "debugsearchindex" attribute type. */ private static DN debugSearchIndexDN; /** * Attribute type corresponding to the "ref" attribute type. Used in * the search reference access check. */ private static AttributeType refAttrType; private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); static { initStatics(); } /** * We initialize these for each new AciHandler so that we can clear out the * stale references that can occur during an in-core restart. */ private static void initStatics() { aciType = getAttributeTypeOrDefault("aci"); globalAciType = getAttributeTypeOrDefault(ATTR_AUTHZ_GLOBAL_ACI); debugSearchIndex = getAttributeTypeOrDefault(SuffixContainer.ATTR_DEBUG_SEARCH_INDEX); refAttrType = getAttributeTypeOrDefault(ATTR_REFERRAL_URL); try { debugSearchIndexDN = DN.valueOf("cn=debugsearch"); } catch (DirectoryException ex) { // Should never happen. } } /** The list that holds that ACIs keyed by the DN of the entry holding the ACI. */ private AciList aciList; /** * The listener that handles ACI changes caused by LDAP operations, * ACI decode failure alert logging and backend initialization ACI list adjustment. */ private AciListenerManager aciListenerMgr; /** Creates a new DSEE-compatible access control handler. */ public AciHandler() { // No implementation required. All initialization should be done in // the intializeAccessControlHandler method. } /** {@inheritDoc} */ @Override public void filterEntry(Operation operation, SearchResultEntry unfilteredEntry, SearchResultEntry filteredEntry) { AciLDAPOperationContainer container = new AciLDAPOperationContainer(operation, ACI_READ, unfilteredEntry); // Proxy access check has already been done for this entry in the // maySend method, set the seen flag to true to bypass any proxy check. container.setSeenEntry(true); boolean skipCheck = skipAccessCheck(operation); if (!skipCheck) { filterEntry(container, filteredEntry); } if (container.hasGetEffectiveRightsControl()) { AciEffectiveRights.addRightsToEntry(this, ((SearchOperation) operation).getAttributes(), container, filteredEntry, skipCheck); } } /** {@inheritDoc} */ @Override public void finalizeAccessControlHandler() { aciListenerMgr.finalizeListenerManager(); AciEffectiveRights.finalizeOnShutdown(); DirectoryServer.deregisterSupportedControl(OID_GET_EFFECTIVE_RIGHTS); } /** {@inheritDoc} */ @Override public void initializeAccessControlHandler( DseeCompatAccessControlHandlerCfg configuration) throws ConfigException, InitializationException { initStatics(); DN configurationDN = configuration.dn(); aciList = new AciList(configurationDN); aciListenerMgr = new AciListenerManager(aciList, configurationDN); processGlobalAcis(configuration); processConfigAcis(); DirectoryServer.registerSupportedControl(OID_GET_EFFECTIVE_RIGHTS); } /** {@inheritDoc} */ @Override public boolean isAllowed(DN entryDN, Operation op, Control control) throws DirectoryException { if (!skipAccessCheck(op)) { Entry e = new Entry(entryDN, null, null, null); AciContainer container = new AciLDAPOperationContainer(op, e, control, ACI_READ | ACI_CONTROL); if (!accessAllowed(container)) { return false; } } if (OID_PROXIED_AUTH_V2.equals(control.getOID()) || OID_PROXIED_AUTH_V1.equals(control.getOID())) { op.setAttachment(ORIG_AUTH_ENTRY, op.getAuthorizationEntry()); } else if (OID_GET_EFFECTIVE_RIGHTS.equals(control.getOID())) { GetEffectiveRightsRequestControl getEffectiveRightsControl; if (control instanceof LDAPControl) { getEffectiveRightsControl = GetEffectiveRightsRequestControl.DECODER.decode(control .isCritical(), ((LDAPControl) control).getValue()); } else { getEffectiveRightsControl = (GetEffectiveRightsRequestControl) control; } op.setAttachment(OID_GET_EFFECTIVE_RIGHTS, getEffectiveRightsControl); } return true; } /** {@inheritDoc} */ @Override public boolean isAllowed(ExtendedOperation operation) { if (skipAccessCheck(operation)) { return true; } Entry e = new Entry(operation.getAuthorizationDN(), null, null, null); final AciContainer container = new AciLDAPOperationContainer(operation, e, (ACI_READ | ACI_EXT_OP)); return accessAllowed(container); } /** {@inheritDoc} */ @Override public boolean isAllowed(LocalBackendAddOperation operation) throws DirectoryException { AciContainer container = new AciLDAPOperationContainer(operation, ACI_ADD); return isAllowed(container, operation) // LDAP add needs a verify ACI syntax step in case any // "aci" attribute types are being added. && verifySyntax(operation.getEntryToAdd(), operation, container.getClientDN()); } /** {@inheritDoc} */ @Override public boolean isAllowed(BindOperation bindOperation) { // Not planned to be implemented. return true; } /** * Check access on compare operations. Note that the attribute type is * unavailable at this time, so this method partially parses the raw * attribute string to get the base attribute type. Options are * ignored. * * @param operation * The compare operation to check access on. * @return True if access is allowed. */ @Override public boolean isAllowed(LocalBackendCompareOperation operation) { AciContainer container = new AciLDAPOperationContainer(operation, ACI_COMPARE); String baseName; String rawAttributeType = operation.getRawAttributeType(); int semicolonPosition = rawAttributeType.indexOf(';'); if (semicolonPosition > 0) { baseName = toLowerCase(rawAttributeType.substring(0, semicolonPosition)); } else { baseName = toLowerCase(rawAttributeType); } container.setCurrentAttributeType(getAttributeTypeOrDefault(baseName)); container.setCurrentAttributeValue(operation.getAssertionValue()); return isAllowed(container, operation); } /** * Check access on delete operations. * * @param operation * The delete operation to check access on. * @return True if access is allowed. */ @Override public boolean isAllowed(LocalBackendDeleteOperation operation) { AciContainer container = new AciLDAPOperationContainer(operation, ACI_DELETE); return isAllowed(container, operation); } /** * Checks access on a modifyDN operation. * * @param operation * The modifyDN operation to check access on. * @return True if access is allowed. */ @Override public boolean isAllowed(ModifyDNOperation operation) { if (skipAccessCheck(operation)) { return true; } final RDN oldRDN = operation.getOriginalEntry().getName().rdn(); final RDN newRDN = operation.getNewRDN(); final DN newSuperiorDN = operation.getNewSuperior(); // If this is a modifyDN move to a new superior, then check if the // superior DN has import access. if (newSuperiorDN != null && !aciCheckSuperiorEntry(newSuperiorDN, operation)) { return false; } // Perform the RDN access checks. boolean rdnChangesAllowed = aciCheckRDNs(operation, oldRDN, newRDN); // If this is a modifyDN move to a new superior, then check if the // original entry DN has export access. if (rdnChangesAllowed && newSuperiorDN != null) { AciContainer container = new AciLDAPOperationContainer( operation, ACI_EXPORT, operation.getOriginalEntry()); if (!oldRDN.equals(newRDN)) { // The RDNs are not equal, skip the proxy check since it was // already performed in the aciCheckRDNs call above. container.setSeenEntry(true); } return accessAllowed(container); } return rdnChangesAllowed; } /** {@inheritDoc} */ @Override public boolean isAllowed(LocalBackendModifyOperation operation) throws DirectoryException { AciContainer container = new AciLDAPOperationContainer(operation, ACI_NULL); return aciCheckMods(container, operation, skipAccessCheck(operation)); } /** {@inheritDoc} */ @Override public boolean isAllowed(SearchOperation searchOperation) { // Not planned to be implemented. return true; } /** {@inheritDoc} */ @Override public boolean isAllowed(Operation operation, Entry entry, SearchFilter filter) throws DirectoryException { if (skipAccessCheck(operation)) { return true; } AciContainer container = new AciLDAPOperationContainer(operation, ACI_READ, entry); return testFilter(container, filter); } /** {@inheritDoc} */ @Override public boolean mayProxy(Entry proxyUser, Entry proxiedUser, Operation op) { if (skipAccessCheck(proxyUser)) { return true; } final AuthenticationInfo authInfo = new AuthenticationInfo(proxyUser, DirectoryServer.isRootDN(proxyUser .getName())); final AciContainer container = new AciLDAPOperationContainer(op, proxiedUser, authInfo, ACI_PROXY); return accessAllowedEntry(container); } /** {@inheritDoc} */ @Override public boolean maySend(DN dn, Operation operation, SearchResultReference reference) { if (skipAccessCheck(operation)) { return true; } // Load the values, a bind rule might want to evaluate them. final AttributeBuilder builder = new AttributeBuilder(refAttrType, ATTR_REFERRAL_URL); builder.addAllStrings(reference.getReferralURLs()); final Entry e = new Entry(dn, null, null, null); e.addAttribute(builder.toAttribute(), null); final SearchResultEntry se = new SearchResultEntry(e); final AciContainer container = new AciLDAPOperationContainer(operation, ACI_READ, se); container.setCurrentAttributeType(refAttrType); return accessAllowed(container); } /** {@inheritDoc} */ @Override public boolean maySend(Operation operation, SearchResultEntry entry) { if (skipAccessCheck(operation)) { return true; } AciContainer container = new AciLDAPOperationContainer(operation, ACI_SEARCH, entry); // Pre/post read controls are associated with other types of operation. if (operation instanceof SearchOperation) { try { if (!testFilter(container, ((SearchOperation) operation).getFilter())) { return false; } } catch (DirectoryException ex) { return false; } } container.clearEvalAttributes(ACI_NULL); container.setRights(ACI_READ); if (!accessAllowedEntry(container)) { return false; } if (!container.hasEvalUserAttributes()) { operation.setAttachment(ALL_USER_ATTRS_MATCHED, ALL_USER_ATTRS_MATCHED); } if (!container.hasEvalOpAttributes()) { operation.setAttachment(ALL_OP_ATTRS_MATCHED, ALL_OP_ATTRS_MATCHED); } return true; } /** * Check access using the specified container. This container will * have all of the information to gather applicable ACIs and perform * evaluation on them. * * @param container * An ACI operation container which has all of the * information needed to check access. * @return True if access is allowed. */ boolean accessAllowed(AciContainer container) { DN dn = container.getResourceDN(); // For ACI_WRITE_ADD and ACI_WRITE_DELETE set the ACI_WRITE // right. if (container.hasRights(ACI_WRITE_ADD) || container.hasRights(ACI_WRITE_DELETE)) { container.setRights(container.getRights() | ACI_WRITE); } // Check if the ACI_SELF right needs to be set (selfwrite right). // Only done if the right is ACI_WRITE, an attribute value is set // and that attribute value is a DN. if (container.getCurrentAttributeValue() != null && container.hasRights(ACI_WRITE) && isAttributeDN(container.getCurrentAttributeType())) { String dnString = null; try { dnString = container.getCurrentAttributeValue().toString(); DN tmpDN = DN.valueOf(dnString); // Have a valid DN, compare to clientDN to see if the ACI_SELF // right should be set. if (tmpDN.equals(container.getClientDN())) { container.setRights(container.getRights() | ACI_SELF); } } catch (DirectoryException ex) { // Log a message and keep going. logger.warn(WARN_ACI_NOT_VALID_DN, dnString); } } // First get all allowed candidate ACIs. List<Aci> candidates = aciList.getCandidateAcis(dn); /* * Create an applicable list of ACIs by target matching each * candidate ACI against the container's target match view. */ createApplicableList(candidates, container); // Evaluate the applicable list. final boolean ret = testApplicableLists(container); // Build summary string if doing geteffectiverights eval. if (container.isGetEffectiveRightsEval()) { container.setEvalSummary( AciEffectiveRights.createSummary(container, ret)); } return ret; } /* * TODO Evaluate performance of this method. TODO Evaluate security * concerns of this method. Logic from this method taken almost * directly from DS6 implementation. I find the work done in the * accessAllowedEntry method, particularly with regard to the entry * test evaluation, to be very confusing and potentially pretty * inefficient. I'm also concerned that the "return "true" inside the * for loop could potentially allow access when it should be denied. */ /** * Check if access is allowed on an entry. Access is checked by * iterating through each attribute of an entry, starting with the * "objectclass" attribute type. If access is allowed on the entry * based on one of it's attribute types, then a possible second access * check is performed. This second check is only performed if an entry * test ACI was found during the earlier successful access check. An * entry test ACI has no "targetattrs" keyword, so allowing access * based on an attribute type only would be incorrect. * * @param container * ACI search container containing all of the information * needed to check access. * @return True if access is allowed. */ boolean accessAllowedEntry(AciContainer container) { // set flag that specifies this is the first attribute evaluated // in the entry container.setIsFirstAttribute(true); for (AttributeType attrType : getAllAttrs(container.getResourceEntry())) { /* * Check if access is allowed. If true, then check to see if an * entry test rule was found (no targetattrs) during target match * evaluation. If such a rule was found, set the current attribute * type to "null" and check access again so that rule is applied. */ container.setCurrentAttributeType(attrType); if (accessAllowed(container)) { if (container.hasEntryTestRule()) { container.setCurrentAttributeType(null); if (!accessAllowed(container) && container.isDenyEval()) { /* * If we failed because of a deny permission-bind rule, we need to * stop and return false. * If we failed because there was no explicit allow rule, then we * grant implicit access to the entry. */ return false; } } return true; } } return false; } /** * Performs an access check against all of the attributes of an entry. The * attributes that fail access are removed from the entry. This method * performs the processing needed for the filterEntry method processing. * * @param container * The search or compare container which has all of the information * needed to filter the attributes for this entry. * @param filteredEntry * The partially filtered search result entry being returned to the * client. */ private void filterEntry(AciContainer container, Entry filteredEntry) { for (AttributeType attrType : getAllAttrs(filteredEntry)) { if (container.hasAllUserAttributes() && !attrType.isOperational()) { continue; } if (container.hasAllOpAttributes() && attrType.isOperational()) { continue; } container.setCurrentAttributeType(attrType); if (!accessAllowed(container)) { filteredEntry.removeAttribute(attrType); } } } /** * Checks to see if a LDAP modification is allowed access. * * @param container * The structure containing the LDAP modifications * @param operation * The operation to check modify privileges on. operation to * check and the evaluation context to apply the check * against. * @param skipAccessCheck * True if access checking should be skipped. * @return True if access is allowed. * @throws DirectoryException * If a modified ACI could not be decoded. */ private boolean aciCheckMods(AciContainer container, LocalBackendModifyOperation operation, boolean skipAccessCheck) throws DirectoryException { Entry resourceEntry = container.getResourceEntry(); DN dn = resourceEntry.getName(); List<Modification> modifications = operation.getModifications(); for (Modification m : modifications) { Attribute modAttr = m.getAttribute(); AttributeType modAttrType = modAttr.getAttributeType(); if (modAttrType.equals(aciType) /* * Check that the operation has modify privileges if it contains * an "aci" attribute type. */ && !operation.getClientConnection().hasPrivilege( Privilege.MODIFY_ACL, operation)) { logger.debug(INFO_ACI_MODIFY_FAILED_PRIVILEGE, container.getResourceDN(), container.getClientDN()); return false; } // This access check handles the case where all attributes of this // type are being replaced or deleted. If only a subset is being // deleted than this access check is skipped. ModificationType modType = m.getModificationType(); if (((modType == ModificationType.DELETE && modAttr.isEmpty()) || modType == ModificationType.REPLACE || modType == ModificationType.INCREMENT) /* * Check if we have rights to delete all values of an attribute * type in the resource entry. */ && resourceEntry.hasAttribute(modAttrType)) { container.setCurrentAttributeType(modAttrType); List<Attribute> attrList = resourceEntry.getAttribute(modAttrType, modAttr.getOptions()); if (attrList != null) { for (Attribute a : attrList) { for (ByteString v : a) { container.setCurrentAttributeValue(v); container.setRights(ACI_WRITE_DELETE); if (!skipAccessCheck && !accessAllowed(container)) { return false; } } } } } if (!modAttr.isEmpty()) { for (ByteString v : modAttr) { container.setCurrentAttributeType(modAttrType); switch (m.getModificationType().asEnum()) { case ADD: case REPLACE: container.setCurrentAttributeValue(v); container.setRights(ACI_WRITE_ADD); if (!skipAccessCheck && !accessAllowed(container)) { return false; } break; case DELETE: container.setCurrentAttributeValue(v); container.setRights(ACI_WRITE_DELETE); if (!skipAccessCheck && !accessAllowed(container)) { return false; } break; case INCREMENT: Entry modifiedEntry = operation.getModifiedEntry(); List<Attribute> modifiedAttrs = modifiedEntry.getAttribute(modAttrType, modAttr.getOptions()); if (modifiedAttrs != null) { for (Attribute attr : modifiedAttrs) { for (ByteString val : attr) { container.setCurrentAttributeValue(val); container.setRights(ACI_WRITE_ADD); if (!skipAccessCheck && !accessAllowed(container)) { return false; } } } } break; } /* * Check if the modification type has an "aci" attribute type. * If so, check the syntax of that attribute value. Fail the * the operation if the syntax check fails. */ if (modAttrType.equals(aciType) || modAttrType.equals(globalAciType)) { try { // A global ACI needs a NULL DN, not the DN of the // modification. if (modAttrType.equals(globalAciType)) { dn = DN.rootDN(); } // validate ACI syntax Aci.decode(v, dn); } catch (AciException ex) { throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, WARN_ACI_MODIFY_FAILED_DECODE.get(dn, ex.getMessage())); } } } } } return true; } /** * Perform all needed RDN checks for the modifyDN operation. The old RDN is * not equal to the new RDN. The access checks are: * <ul> * <li>Verify WRITE access to the original entry.</li> * <li>Verify WRITE_ADD access on each RDN component of the new RDN. The * WRITE_ADD access is used because this access could be restricted by the * targattrfilters keyword.</li> * <li>If the deleteOLDRDN flag is set, verify WRITE_DELETE access on the old * RDN. The WRITE_DELETE access is used because this access could be * restricted by the targattrfilters keyword. * <li> * </ul> * * @param operation * The ModifyDN operation class containing information to check * access on. * @param oldRDN * The old RDN component. * @param newRDN * The new RDN component. * @return True if access is allowed. */ private boolean aciCheckRDNs(ModifyDNOperation operation, RDN oldRDN, RDN newRDN) { AciContainer container = new AciLDAPOperationContainer(operation, ACI_WRITE, operation .getOriginalEntry()); if (!accessAllowed(container)) { return false; } boolean ret = checkRDN(ACI_WRITE_ADD, newRDN, container); if (ret && operation.deleteOldRDN()) { ret = checkRDN(ACI_WRITE_DELETE, oldRDN, container); } return ret; } /** * Check access on the new superior entry if it exists. If superiordn is null, * the entry does not exist or the DN cannot be locked then false is returned. * * @param superiorDN * The DN of the new superior entry. * @param op * The modifyDN operation to check access on. * @return True if access is granted to the new superior entry. */ private boolean aciCheckSuperiorEntry(DN superiorDN, ModifyDNOperation op) { try { Entry superiorEntry = DirectoryServer.getEntry(superiorDN); if (superiorEntry != null) { AciContainer container = new AciLDAPOperationContainer(op, ACI_IMPORT, superiorEntry); return accessAllowed(container); } return false; } catch (DirectoryException ex) { return false; } } /** * Check access on each attribute-value pair component of the * specified RDN. There may be more than one attribute-value pair if * the RDN is multi-valued. * * @param right * The access right to check for. * @param rdn * The RDN to examine the attribute-value pairs of. * @param container * The container containing the information needed to * evaluate the specified RDN. * @return True if access is allowed for all attribute-value pairs. */ private boolean checkRDN(int right, RDN rdn, AciContainer container) { container.setRights(right); final int numAVAs = rdn.getNumValues(); for (int i = 0; i < numAVAs; i++) { container.setCurrentAttributeType(rdn.getAttributeType(i)); container.setCurrentAttributeValue(rdn.getAttributeValue(i)); if (!accessAllowed(container)) { return false; } } return true; } /** * Creates the allow and deny ACI lists based on the provided target * match context. These lists are stored in the evaluation context. * * @param candidates * List of all possible ACI candidates. * @param targetMatchCtx * Target matching context to use for testing each ACI. */ private void createApplicableList(List<Aci> candidates, AciTargetMatchContext targetMatchCtx) { List<Aci> denys = new LinkedList<>(); List<Aci> allows = new LinkedList<>(); for (Aci aci : candidates) { if (Aci.isApplicable(aci, targetMatchCtx)) { if (aci.hasAccessType(EnumAccessType.DENY)) { denys.add(aci); } if (aci.hasAccessType(EnumAccessType.ALLOW)) { allows.add(aci); } } if (targetMatchCtx.getTargAttrFiltersMatch()) { targetMatchCtx.setTargAttrFiltersMatch(false); } } targetMatchCtx.setAllowList(allows); targetMatchCtx.setDenyList(denys); } /** * Gathers all of the attribute types in an entry along with the * "objectclass" attribute type in a List. The "objectclass" attribute * is added to the list first so it is evaluated first. * * @param e * Entry to gather the attributes for. * @return List containing the attribute types. */ private List<AttributeType> getAllAttrs(Entry e) { List<AttributeType> typeList = new LinkedList<>(); /* * When a search is not all attributes returned, the "objectclass" * attribute type is missing from the entry. */ final Attribute attr = e.getObjectClassAttribute(); if (attr != null) { AttributeType ocType = attr.getAttributeType(); typeList.add(ocType); } typeList.addAll(e.getUserAttributes().keySet()); typeList.addAll(e.getOperationalAttributes().keySet()); return typeList; } /** * Check access using the accessAllowed method. The LDAP add, compare, * modify and delete operations use this function. The other supported * LDAP operations have more specialized checks. * * @param container * The container containing the information needed to * evaluate this operation. * @param operation * The operation being evaluated. * @return True if this operation is allowed access. */ private boolean isAllowed(AciContainer container, Operation operation) { return skipAccessCheck(operation) || accessAllowed(container); } /** * Check if the specified attribute type is a DN by checking if its * syntax OID is equal to the DN syntax OID. * * @param attribute * The attribute type to check. * @return True if the attribute type syntax OID is equal to a DN * syntax OID. */ private boolean isAttributeDN(AttributeType attribute) { return SYNTAX_DN_OID.equals(attribute.getSyntax().getOID()); } /** * Process all ACIs under the "cn=config" naming context and adds them * to the ACI list cache. It also logs messages about the number of * ACIs added to the cache. This method is called once at startup. It * will put the server in lockdown mode if needed. * * @throws InitializationException * If there is an error searching for the ACIs in the naming * context. */ private void processConfigAcis() throws InitializationException { LinkedList<LocalizableMessage> failedACIMsgs = new LinkedList<>(); InternalClientConnection conn = getRootConnection(); ConfigHandler<?> configBackend = DirectoryServer.getConfigHandler(); for (DN baseDN : configBackend.getBaseDNs()) { try { if (! configBackend.entryExists(baseDN)) { continue; } } catch (Exception e) { logger.traceException(e); // FIXME -- Is there anything that we need to do here? continue; } try { SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, "aci=*").addAttribute("aci"); InternalSearchOperation internalSearch = new InternalSearchOperation(conn, nextOperationID(), nextMessageID(), request); LocalBackendSearchOperation localSearch = new LocalBackendSearchOperation(internalSearch); configBackend.search(localSearch); if (!internalSearch.getSearchEntries().isEmpty()) { int validAcis = aciList.addAci(internalSearch.getSearchEntries(), failedACIMsgs); if (!failedACIMsgs.isEmpty()) { aciListenerMgr.logMsgsSetLockDownMode(failedACIMsgs); } logger.debug(INFO_ACI_ADD_LIST_ACIS, validAcis, baseDN); } } catch (Exception e) { LocalizableMessage message = INFO_ACI_HANDLER_FAIL_PROCESS_ACI.get(); throw new InitializationException(message, e); } } } /** * Process all global ACI attribute types found in the configuration * entry and adds them to that ACI list cache. It also logs messages * about the number of ACI attribute types added to the cache. This * method is called once at startup. It also will put the server into * lockdown mode if needed. * * @param configuration * The config handler containing the ACI configuration * information. * @throws InitializationException * If there is an error reading the global ACIs from the * configuration entry. */ private void processGlobalAcis( DseeCompatAccessControlHandlerCfg configuration) throws InitializationException { try { final SortedSet<Aci> globalAcis = configuration.getGlobalACI(); if (globalAcis != null) { aciList.addAci(DN.rootDN(), globalAcis); logger.debug(INFO_ACI_ADD_LIST_GLOBAL_ACIS, globalAcis.size()); } } catch (Exception e) { logger.traceException(e); throw new InitializationException( INFO_ACI_HANDLER_FAIL_PROCESS_GLOBAL_ACI.get(configuration.dn()), e); } } /** * Check to see if the specified entry has the specified privilege. * * @param e * The entry to check privileges on. * @return {@code true} if the entry has the specified privilege, or * {@code false} if not. */ private boolean skipAccessCheck(Entry e) { return ClientConnection.hasPrivilege(e, Privilege.BYPASS_ACL); } /** * Check to see if the client entry has BYPASS_ACL privileges for this * operation. * * @param operation * The operation to check privileges on. * @return True if access checking can be skipped because the * operation client connection has BYPASS_ACL privileges. */ private boolean skipAccessCheck(Operation operation) { return operation.getClientConnection().hasPrivilege( Privilege.BYPASS_ACL, operation); } /** * Performs the test of the deny and allow access lists using the * provided evaluation context. The deny list is checked first. * * @param evalCtx * The evaluation context to use. * @return True if access is allowed. */ private boolean testApplicableLists(AciEvalContext evalCtx) { evalCtx.setEvaluationResult(NO_REASON, null); if (evalCtx.getAllowList().isEmpty() && (!evalCtx.isGetEffectiveRightsEval() || evalCtx.hasRights(ACI_SELF) || !evalCtx.isTargAttrFilterMatchAciEmpty())) { // If allows list is empty and not doing geteffectiverights return false. evalCtx.setEvaluationResult(NO_ALLOW_ACIS, null); return false; } for (Aci denyAci : evalCtx.getDenyList()) { final EnumEvalResult res = Aci.evaluate(evalCtx, denyAci); // Failure could be returned if a system limit is hit or // search fails if (res.equals(EnumEvalResult.FAIL)) { evalCtx.setEvaluationResult(EVALUATED_DENY_ACI, denyAci); return false; } else if (res.equals(EnumEvalResult.TRUE)) { if (testAndSetTargAttrOperationMatches(evalCtx, denyAci, true)) { continue; } evalCtx.setEvaluationResult(EVALUATED_DENY_ACI, denyAci); return false; } } for (Aci allowAci : evalCtx.getAllowList()) { final EnumEvalResult res = Aci.evaluate(evalCtx, allowAci); if (res.equals(EnumEvalResult.TRUE)) { if (testAndSetTargAttrOperationMatches(evalCtx, allowAci, false)) { continue; } evalCtx.setEvaluationResult(EVALUATED_ALLOW_ACI, allowAci); return true; } } // Nothing matched fall through. evalCtx.setEvaluationResult(NO_MATCHED_ALLOWS_ACIS, null); return false; } private boolean testAndSetTargAttrOperationMatches(AciEvalContext evalCtx, Aci aci, boolean isDenyAci) { return evalCtx.isGetEffectiveRightsEval() && !evalCtx.hasRights(ACI_SELF) && !evalCtx.isTargAttrFilterMatchAciEmpty() // Iterate to next only if ACI contains a targattrfilters keyword. && AciEffectiveRights.setTargAttrAci(evalCtx, aci, isDenyAci); } /** * Test the attribute types of the search filter for access. This * method supports the search right. * * @param container * The container used in the access evaluation. * @param filter * The filter to check access on. * @return True if all attribute types in the filter have access. * @throws DirectoryException * If there is a problem matching the entry using the * provided filter. */ private boolean testFilter(AciContainer container, SearchFilter filter) throws DirectoryException { // If the resource entry has a dn equal to "cn=debugsearch" and it // contains the special attribute type "debugsearchindex", then the // resource entry is a pseudo entry created for debug purposes. // Return true if that is the case. if (debugSearchIndexDN.equals(container.getResourceDN()) && container.getResourceEntry().hasAttribute(debugSearchIndex)) { return true; } switch (filter.getFilterType()) { case AND: case OR: { for (SearchFilter f : filter.getFilterComponents()) { if (!testFilter(container, f)) { return false; } } break; } case NOT: { return testFilter(container, filter.getNotComponent()); } default: { container.setCurrentAttributeType(filter.getAttributeType()); return accessAllowed(container); } } return true; } /** * Evaluate an entry to be added to see if it has any "aci" attribute * type. If it does, examines each "aci" attribute type value for * syntax errors. All of the "aci" attribute type values must pass * syntax check for the add operation to proceed. Any entry with an * "aci" attribute type must have "modify-acl" privileges. * * @param entry * The entry to be examined. * @param operation * The operation to to check privileges on. * @param clientDN * The authorization DN. * @return True if the entry has no ACI attributes or if all of the * "aci" attributes values pass ACI syntax checking. * @throws DirectoryException * If a modified ACI could not be decoded. */ private boolean verifySyntax(Entry entry, Operation operation, DN clientDN) throws DirectoryException { if (entry.hasOperationalAttribute(aciType)) { /* * Check that the operation has "modify-acl" privileges since the * entry to be added has an "aci" attribute type. */ if (!operation.getClientConnection().hasPrivilege( Privilege.MODIFY_ACL, operation)) { logger.debug(INFO_ACI_ADD_FAILED_PRIVILEGE, entry.getName(), clientDN); return false; } List<Attribute> attributeList = entry.getOperationalAttribute(aciType, null); for (Attribute attribute : attributeList) { for (ByteString value : attribute) { try { // validate ACI syntax Aci.decode(value, entry.getName()); } catch (AciException ex) { throw new DirectoryException( ResultCode.INVALID_ATTRIBUTE_SYNTAX, WARN_ACI_ADD_FAILED_DECODE.get(entry.getName(), ex.getMessage())); } } } } return true; } }