/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 * * * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.authorization.dseecompat; import org.opends.server.api.AccessControlHandler; import static org.opends.server.authorization.dseecompat.AciMessages.*; import org.opends.server.core.*; import static org.opends.server.loggers.Debug.debugEnter; import static org.opends.server.loggers.Error.logError; import static org.opends.server.messages.MessageHandler.getMessage; import org.opends.server.types.*; import static org.opends.server.util.StaticUtils.toLowerCase; import java.util.LinkedList; import java.util.List; import java.util.Map; /** * The AciHandler class performs the main processing for the * dseecompat package. */ public class AciHandler extends AccessControlHandler { private static final String CLASS_NAME = "org.opends.server.authorization.dseecompat.AciHandler"; /** * ACI_ADD is used to set the container rights for a LDAP add operation. */ public static final int ACI_ADD = 0x0001; /** * ACI_DELETE is used to set the container rights for a LDAP * delete operation. */ public static final int ACI_DELETE = 0x0002; /** * ACI_READ is used to set the container rights for a LDAP * search operation. */ public static final int ACI_READ = 0x0004; /** * ACI_WRITE is used to set the container rights for a LDAP * modify operation. */ public static final int ACI_WRITE = 0x0008; /** * ACI_COMPARE is used to set the container rights for a LDAP * compare operation. */ public static final int ACI_COMPARE = 0x0010; /** * ACI_SEARCH is used to set the container rights a LDAP search operation. */ public static final int ACI_SEARCH = 0x0020; /** * ACI_SELF is used for the SELFWRITE right. Currently not implemented. */ public static final int ACI_SELF = 0x0040; /** * ACI_ALL is used to as a mask for all of the above. These * six below are not masked by the ACI_ALL. */ public static final int ACI_ALL = 0x007F; /** * ACI_PROXY is used for the PROXY right. Currently not implemented. */ public static final int ACI_PROXY = 0x0080; /** * ACI_IMPORT is used to set the container rights for a LDAP * modify dn operation. Currently not implemented. */ public static final int ACI_IMPORT = 0x0100; /** * ACI_EXPORT is used to set the container rights for a LDAP * modify dn operation. Currently not implemented. */ public static final int ACI_EXPORT = 0x0200; /** * ACI_WRITE_ADD and ACI_WRITE_DELETE are used by the LDAP modify * operation. They currently don't have much value; but will be needed * once the targetattrfilters target and modify dn are implemented. */ public static final int ACI_WRITE_ADD = 0x800; /** * See above. */ public static final int ACI_WRITE_DELETE = 0x400; /** * ACI_NULL is used to set the container rights to all zeros. Used * by LDAP modify. */ public static final int ACI_NULL = 0x0000; /* * The list that holds that ACIs keyed by the DN of the entry * holding the ACI. */ private AciList aciList; /** * Attribute type corresponding to "aci" attribute. */ public static AttributeType aciType; /** * Constructor that registers the message catalog, creates the ACI list * class that manages the ACI list. Instantiates and registers the change * notification listener that is used to manage the ACI list on * modifications and the backend initialization listener that is used to * register/de-register aci attribute types in backends when backends * are initialized/finalized. */ public AciHandler() { AciMessages.registerMessages(); aciList = new AciList(); AciListenerManager aciListenerMgr = new AciListenerManager(aciList); DirectoryServer.registerChangeNotificationListener(aciListenerMgr); DirectoryServer.registerBackendInitializationListener(aciListenerMgr); if((aciType = DirectoryServer.getAttributeType("aci")) == null) aciType = DirectoryServer.getDefaultAttributeType("aci"); } /* * TODO * The internal search performed by the searchAcis method will require * a presence index on the aci attribute for any database of any significant * size. We should probably consider making this index present by default, * because if they aren't using the DSEE-compatible implementation then * they probably won't have any instances of the aci attribute. */ /** * 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. */ private boolean aciCheckMods(AciLDAPOperationContainer container, Operation operation, boolean skipAccessCheck) { Entry resourceEntry=container.getResourceEntry(); DN dn=resourceEntry.getDN(); List modifications=container.getModifications(); for(Modification m : modifications) { Attribute modAttr=m.getAttribute(); AttributeType modType=modAttr.getAttributeType(); switch(m.getModificationType()) { /* * TODO Increment modification type needs to be handled. */ case DELETE: case REPLACE: { /* Check if we have rights to delete all values of an attribute type in the resource entry. */ if(resourceEntry.hasAttribute(modType)) { container.setCurrentAttributeType(modType); List attrList = resourceEntry.getAttribute(modType,null); for (Attribute a : attrList) { for (AttributeValue v : a.getValues()) { container.setCurrentAttributeValue(v); container.setRights(ACI_WRITE_DELETE); if(!skipAccessCheck && !accessAllowed(container)) return false; } } } } } if(modAttr.hasValue()) { boolean checkPrivileges=true; for(AttributeValue v : modAttr.getValues()) { container.setCurrentAttributeType(modType); container.setCurrentAttributeValue(v); if((m.getModificationType() == ModificationType.ADD) || (m.getModificationType() == ModificationType.REPLACE)) { container.setRights(ACI_WRITE_ADD); if(!skipAccessCheck && !accessAllowed(container)) return false; } else if(m.getModificationType() == ModificationType.DELETE) { container.setRights(ACI_WRITE_DELETE); if(!skipAccessCheck && !accessAllowed(container)) return false; } else return false; /* 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(modType.equals(aciType)) { try { /* * Check that the operation has modify privileges if * it contains an "aci" attribute type. Flip the * boolean to false so this check isn't made again * if there are several ACI values being added. */ if(checkPrivileges) { if (!operation.getClientConnection(). hasPrivilege(Privilege.MODIFY_ACL, operation)) { int msgID = MSGID_ACI_MODIFY_FAILED_PRIVILEGE; String message = getMessage(msgID, String.valueOf(container.getResourceDN()), String.valueOf(container.getClientDN())); logError(ErrorLogCategory.ACCESS_CONTROL, ErrorLogSeverity.SEVERE_WARNING, message, msgID); return false; } checkPrivileges=false; } Aci.decode(v.getValue(),dn); } catch (AciException ex) { int msgID = MSGID_ACI_MODIFY_FAILED_DECODE; String message = getMessage(msgID, String.valueOf(dn), ex.getMessage()); logError(ErrorLogCategory.ACCESS_CONTROL, ErrorLogSeverity.SEVERE_WARNING, message, msgID); return false; } } } } } return true; } /** * 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) { EnumEvalResult res=EnumEvalResult.FALSE; //First check deny lists LinkedListdenys=evalCtx.getDenyList(); evalCtx.setDenyEval(true); for(Aci denyAci : denys) { res=Aci.evaluate(evalCtx, denyAci); //Failure could be returned if a system limit is hit or //search fails if((res.equals(EnumEvalResult.FAIL) || (res.equals(EnumEvalResult.TRUE)))) { return false; } } //Now check the allows -- flip the deny flag to false first. evalCtx.setDenyEval(false); LinkedListallows=evalCtx.getAllowList(); for(Aci allowAci : allows) { res=Aci.evaluate(evalCtx, allowAci); if(res.equals(EnumEvalResult.TRUE)) { break; } } return res.getBoolVal(); } /** * 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(LinkedList candidates, AciTargetMatchContext targetMatchCtx) { LinkedListdenys=new LinkedList(); LinkedListallows=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); } } } targetMatchCtx.setAllowList(allows); targetMatchCtx.setDenyList(denys); } /** * 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. */ boolean skipAccessCheck(Operation operation) { return operation.getClientConnection(). hasPrivilege(Privilege.BYPASS_ACL, operation); } /** * 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. */ private boolean accessAllowed(AciContainer container) { DN dn = container.getResourceEntry().getDN(); //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); /* * First get all allowed candidate ACIs. */ LinkedListcandidates = 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); /* * Lastly, evaluate the applicable list. */ return(testApplicableLists(container)); } /** * 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. * @return The entry to send back to the client, minus any attribute * types that failed access check. */ private SearchResultEntry accessAllowedAttrs(AciLDAPOperationContainer container) { Entry e=container.getResourceEntry(); List typeList=getAllAttrs(e); for(AttributeType attrType : typeList) { container.setCurrentAttributeType(attrType); if(!accessAllowed(container)) { e.removeAttribute(attrType); } } return container.getSearchResultEntry(); } /** * 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 getAllAttrs(Entry e) { Map> attrMap = e.getUserAttributes(); List typeList=new LinkedList(); Attribute attr=e.getObjectClassAttribute(); /* * When a search is not all attributes returned, the "objectclass" * attribute type is missing from the entry. */ if(attr != null) { AttributeType ocType=attr.getAttributeType(); typeList.add(ocType); } typeList.addAll(attrMap.keySet()); return typeList; } /* * 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. */ private boolean accessAllowedEntry(AciLDAPOperationContainer container) { boolean ret=false; //set flag that specifies this is the first attribute evaluated //in the entry container.setIsFirstAttribute(true); List typeList=getAllAttrs(container.getResourceEntry()); for(AttributeType attrType : typeList) { container.setCurrentAttributeType(attrType); /* * 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. */ if(accessAllowed(container)) { if(container.hasEntryTestRule()) { container.setCurrentAttributeType(null); if(!accessAllowed(container)) { /* * If we failed because of a deny permission-bind rule, * we need to stop and return false. */ if(container.isDenyEval()) { return false; } /* * If we failed because there was no explicit * allow rule, then we grant implicit access to the * entry. */ } } return true; } } return ret; } /** * 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. */ private boolean verifySyntax(Entry entry, Operation operation, DN clientDN) { 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)) { int msgID = MSGID_ACI_ADD_FAILED_PRIVILEGE; String message = getMessage(msgID, String.valueOf(entry.getDN()), String.valueOf(clientDN)); logError(ErrorLogCategory.ACCESS_CONTROL, ErrorLogSeverity.SEVERE_WARNING, message, msgID); return false; } List attributeList = entry.getOperationalAttribute(aciType, null); for (Attribute attribute : attributeList) { for (AttributeValue value : attribute.getValues()) { try { DN dn=entry.getDN(); Aci.decode(value.getValue(),dn); } catch (AciException ex) { int msgID = MSGID_ACI_ADD_FAILED_DECODE; String message = getMessage(msgID, String.valueOf(entry.getDN()), ex.getMessage()); logError(ErrorLogCategory.ACCESS_CONTROL, ErrorLogSeverity.SEVERE_WARNING, message, msgID); return false; } } } } return true; } /** * 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 operationContainer 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(AciLDAPOperationContainer operationContainer, Operation operation) { return skipAccessCheck(operation) || accessAllowed(operationContainer); } /** * Check access on add operations. * * @param operation The add operation to check access on. * @return True if access is allowed. */ public boolean isAllowed(AddOperation operation) { assert debugEnter(CLASS_NAME, "isAllowed"); AciLDAPOperationContainer operationContainer = new AciLDAPOperationContainer(operation, ACI_ADD); boolean ret=isAllowed(operationContainer,operation); //LDAP add needs a verify ACI syntax step in case any //"aci" attribute types are being added. if(ret) ret=verifySyntax(operation.getEntryToAdd(), operation, operationContainer.getClientDN()); return ret; } /** * 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. */ public boolean isAllowed(CompareOperation operation) { assert debugEnter(CLASS_NAME, "isAllowed"); AciLDAPOperationContainer operationContainer = 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); AttributeType attributeType; if((attributeType = DirectoryServer.getAttributeType(baseName)) == null) attributeType = DirectoryServer.getDefaultAttributeType(baseName); AttributeValue attributeValue = new AttributeValue(attributeType, operation.getAssertionValue()); operationContainer.setCurrentAttributeType(attributeType); operationContainer.setCurrentAttributeValue(attributeValue); return isAllowed(operationContainer, operation); } /** * Check access on delete operations. * * @param operation The delete operation to check access on. * @return True if access is allowed. */ public boolean isAllowed(DeleteOperation operation) { assert debugEnter(CLASS_NAME, "isAllowed"); AciLDAPOperationContainer operationContainer= new AciLDAPOperationContainer(operation, ACI_DELETE); return isAllowed(operationContainer, operation); } /** * Check access on modify operations. * * @param operation The modify operation to check access on. * @return True if access is allowed. */ public boolean isAllowed(ModifyOperation operation) { assert debugEnter(CLASS_NAME, "isAllowed"); AciLDAPOperationContainer operationContainer= new AciLDAPOperationContainer(operation, ACI_NULL); return aciCheckMods(operationContainer, operation, skipAccessCheck(operation)); } /* * TODO Add access testing of the filter against the entry. This was * brought up in the first code review. * * The static block that creates the arrays of EnumRight objects needs to * be documented to explain what they are. Also, I still disagree with * the interpretation that the READ right is all that is necessary to * perform either search or compare operations. That definitely goes * against the documentation, which states that READ applies only to * the search operation, and that users must have both SEARCH and READ * in order to access the results. */ /** * Checks access on a search operation. * @param operation The search operation class containing information to * check the access on. * @param entry The entry to evaluate access. * @return True if access is allowed. */ public boolean maySend(SearchOperation operation, SearchResultEntry entry) { assert debugEnter(CLASS_NAME, "maySend"); AciLDAPOperationContainer operationContainer = new AciLDAPOperationContainer(operation, (ACI_READ | ACI_SEARCH), entry); return skipAccessCheck(operation) || accessAllowedEntry(operationContainer); } /* * TODO Rename this method. Needs to be changed in SearchOperation. * * I find the name of the filterEntry method to be misleading because * it works on a search operation but has nothing to do with the search * filter. Something like "removeDisallowedAttributes" would be clearer. */ /** * Checks access on each attribute in an entry. It removes those attributes * that fail access check. * * @param operation The search operation class containing information to * check access on. * @param entry The entry containing the attributes. * @return The entry to return minus filtered attributes. */ public SearchResultEntry filterEntry(SearchOperation operation, SearchResultEntry entry) { assert debugEnter(CLASS_NAME, "filterEntry"); AciLDAPOperationContainer operationContainer = new AciLDAPOperationContainer(operation, (ACI_READ | ACI_SEARCH), entry); SearchResultEntry returnEntry; if(!skipAccessCheck(operation)) { returnEntry=accessAllowedAttrs(operationContainer); } else returnEntry=entry; return returnEntry; } //Planned to be implemented methods /** * {@inheritDoc} */ @Override public boolean maySend(SearchOperation operation, SearchResultReference reference) { assert debugEnter(CLASS_NAME, "maySend"); //TODO: Planned to be implemented. return true; } /** * {@inheritDoc} */ @Override public boolean isAllowed(ModifyDNOperation modifyDNOperation) { assert debugEnter(CLASS_NAME, "isAllowed"); // TODO: Planned to be implemented. return true; } //Not planned to be implemented methods. /** * {@inheritDoc} */ @Override public boolean isAllowed(BindOperation bindOperation) { assert debugEnter(CLASS_NAME, "isAllowed"); //Not planned to be implemented. return true; } /** * {@inheritDoc} */ @Override public boolean isAllowed(ExtendedOperation extendedOperation) { assert debugEnter(CLASS_NAME, "isAllowed"); //Not planned to be implemented. return true; } /** * {@inheritDoc} */ @Override public boolean isAllowed(SearchOperation searchOperation) { assert debugEnter(CLASS_NAME, "isAllowed"); //Not planned to be implemented. return true; } }