/* * Part of the CCNx Java Library. * * Copyright (C) 2008-2012 Palo Alto Research Center, Inc. * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 2.1 * as published by the Free Software Foundation. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. You should have received * a copy of the GNU Lesser General Public License along with this library; * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, * Fifth Floor, Boston, MA 02110-1301 USA. */ package org.ccnx.ccn.profiles.security.access.group; import java.io.IOException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.Iterator; import java.util.SortedSet; import java.util.logging.Level; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.config.ConfigurationException; import org.ccnx.ccn.config.SystemConfiguration; import org.ccnx.ccn.impl.CCNFlowControl.SaveType; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.ErrorStateException; import org.ccnx.ccn.io.content.Collection; import org.ccnx.ccn.io.content.ContentDecodingException; import org.ccnx.ccn.io.content.ContentEncodingException; import org.ccnx.ccn.io.content.ContentGoneException; import org.ccnx.ccn.io.content.ContentNotReadyException; import org.ccnx.ccn.io.content.Link; import org.ccnx.ccn.io.content.LinkAuthenticator; import org.ccnx.ccn.io.content.PublicKeyObject; import org.ccnx.ccn.io.content.WrappedKey; import org.ccnx.ccn.io.content.Link.LinkObject; import org.ccnx.ccn.profiles.VersionMissingException; import org.ccnx.ccn.profiles.VersioningProfile; import org.ccnx.ccn.profiles.nameenum.EnumeratedNameList; import org.ccnx.ccn.profiles.namespace.ParameterizedName; import org.ccnx.ccn.profiles.security.access.AccessDeniedException; import org.ccnx.ccn.protocol.CCNTime; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.PublisherID; /** * This class represents a Group for group-based access control. A Group * is essentially a list of members, and a public/private key pair. The * public key is stored in CCN and is used to encrypt node keys (see CCNx * Access Control Specification); the private key is stored encrypted under * the public keys of the members of the group (which could be users or * groups). The private key is represented in a KeyDirectory. * * Model for private key access: if you're not allowed to get a key, * we throw AccessDeniedException. * * Right now dynamically load both public key and membership list. * For efficiency might want to only load public key, and pull membership * list only when we need to. * */ public class Group { private static final long PARENT_GROUP_ENUMERATION_TIMEOUT = 3000; private ParameterizedName _groupNamespace; private PublicKeyObject _groupPublicKey; private MembershipListObject _groupMembers; private String _groupFriendlyName; private CCNHandle _handle; private GroupManager _groupManager; /** * The <KeyDirectory> which stores the group private key wrapped * in the public keys of the members of the group. */ private PrincipalKeyDirectory _privKeyDirectory = null; /** * Group constructor * @param namespace the group namespace * @param groupFriendlyName the friendly name by which the group is known * @param handle the CCN handle * @param manager the group manager * @throws IOException * @throws ContentDecodingException */ public Group(ParameterizedName groupNamespace, String groupFriendlyName, CCNHandle handle,GroupManager manager) throws ContentDecodingException, IOException { _handle = handle; _groupNamespace = groupNamespace; _groupFriendlyName = groupFriendlyName; _groupPublicKey = new PublicKeyObject(GroupAccessControlProfile.groupPublicKeyName(_groupNamespace, _groupFriendlyName), _handle); _groupPublicKey.updateInBackground(true); _groupManager = manager; } /** * Constructor * @param groupName * @param handle * @param manager * @throws IOException * @throws ContentDecodingException */ public Group(ContentName groupName, CCNHandle handle, GroupManager manager) throws ContentDecodingException, IOException { this(manager.getGroupStorage(), GroupAccessControlProfile.groupNameToFriendlyName(groupName), handle,manager); } /** * Constructor that creates a new group and generates a first key pair for it. * @param namespace the group namespace * @param groupFriendlyName the friendly name by which the group is known * @param members the membership list of the group * @param handle the CCN handle * @param manager the group manager * @throws IOException * @throws ContentEncodingException * @throws ConfigurationException * @throws InvalidKeyException */ Group(ParameterizedName groupNamespace, String groupFriendlyName, MembershipListObject members, CCNHandle handle, GroupManager manager) throws ContentEncodingException, IOException, InvalidKeyException { _handle = handle; _groupNamespace = groupNamespace; _groupFriendlyName = groupFriendlyName; _groupManager = manager; _groupMembers = members; _groupMembers.save(); createGroupPublicKey(members); } /** * Add new users to an existing group * @param newUsers the list of new users * @throws IOException * @throws ConfigurationException * @throws ContentDecodingException * @throws InvalidKeyException * @throws NoSuchAlgorithmException */ public void addMembers(ArrayList<Link> newUsers) throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException { modify(newUsers, null); } /** * Remove users from an existing group * @param removedUsers the list of users to be removed. * @throws IOException * @throws ConfigurationException * @throws ContentDecodingException * @throws InvalidKeyException * @throws NoSuchAlgorithmException */ public void removeMembers( ArrayList<Link> removedUsers) throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException { modify(null, removedUsers); } /** * Checks whether the group public key has been created. * @return */ public boolean ready() { return _groupPublicKey.available(); } /** * Returns the KeyDirectory which stores the group private key wrapped * in the public keys of the members of the group. * A new private key directory is created if it does not already exist * and if the group public key is ready. * @param manager the access control manager * @return the key directory of the group * @throws IOException */ public PrincipalKeyDirectory privateKeyDirectory(GroupAccessControlManager manager) throws IOException { if (_privKeyDirectory != null) { // check that our version of KeyDirectory is not stale if (_privKeyDirectory.getName().equals(GroupAccessControlProfile.groupPrivateKeyDirectory(_groupPublicKey.getVersionedName()))) { return _privKeyDirectory; } } if (_groupPublicKey.available()) { _privKeyDirectory = new PrincipalKeyDirectory(manager, GroupAccessControlProfile.groupPrivateKeyDirectory(_groupPublicKey.getVersionedName()), _handle); return _privKeyDirectory; } if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "Public key not ready for group: " + friendlyName()); } return null; } /** * Stop enumerating the private key directory. * @throws IOException */ protected void stopPrivateKeyDirectoryEnumeration() throws IOException { if (_privKeyDirectory != null) { _privKeyDirectory.stopEnumerating(); } } /** * Restart the enumeration of the private key directory. * @param manager the access control manager. * @throws IOException */ public void restartPrivateKeyDirectoryEnumeration(GroupAccessControlManager manager) throws IOException { stopPrivateKeyDirectoryEnumeration(); _privKeyDirectory = null; privateKeyDirectory(manager); } /** * Get the friendly name by which the group is known * @return the group friendly name. */ public String friendlyName() { return _groupFriendlyName; } /** * Get the name of the namespace for the group * @return the group namespace */ public ContentName groupName() {return new ContentName(_groupNamespace.prefix(), _groupFriendlyName); } /** * Returns a list containing all the members of a Group. * Sets up the list to automatically update in the background. * @return MembershipList a list containing all the members of a Group object * @throws ContentDecodingException * @throws IOException */ public MembershipListObject membershipList() throws ContentDecodingException, IOException { if (null == _groupMembers) { // Read constructor. Synchronously updates. // Throws an exception if no membership list is found or upon error. // Reading membership list from network. Needs error handling. _groupMembers = new MembershipListObject(GroupAccessControlProfile.groupMembershipListName(_groupNamespace, _groupFriendlyName), _handle); // Keep dynamically updating. _groupMembers.updateInBackground(true); _groupMembers.setupSave(SaveType.REPOSITORY); } return _groupMembers; } /** * Get the versioned name of the group membership list * @return the versioned name of the group membership list * @throws IOException * @throws ContentDecodingException */ public ContentName membershipListName() throws ContentDecodingException, IOException { return membershipList().getVersionedName(); } /** * Get the version of the membership list * @return the version of the membership list * @throws IOException * @throws ContentDecodingException */ public CCNTime membershipListVersion() throws ContentDecodingException, IOException { ContentName name = membershipListName(); if (VersioningProfile.hasTerminalVersion(name)) { try { return VersioningProfile.getLastVersionAsTimestamp(name); } catch (VersionMissingException e) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Should not happen: VersionMissingException on name where isVersioned is true: " + name + ": " + e.getMessage()); } } } return null; } /** * Clear the cached membership list. * This does not actually remove any members from the group, it just * clears out our in-memory copy of the membership list. */ public void clearCachedMembershipList() { if (null != _groupMembers) { _groupMembers.cancelInterest(); // stop updating _groupMembers = null; } } /** * Get the public key of the group * @return the group public key */ public PublicKeyObject publicKeyObject() { return _groupPublicKey; } /** * Get the group public key * @return the group public key * @throws ContentNotReadyException * @throws ContentGoneException * @throws ErrorStateException */ public PublicKey publicKey() throws ContentNotReadyException, ContentGoneException, ErrorStateException { return _groupPublicKey.publicKey(); } /** * Get the versioned name of the group public key * @return the versioned name of the group public key */ public ContentName publicKeyName() { return _groupPublicKey.getVersionedName(); } /** * Get the version of the group public key * @return the version of the group public key * @throws IOException */ public CCNTime publicKeyVersion() throws IOException { return _groupPublicKey.getVersion(); } /** * Sets the membership list of the group. Existing members of the group are removed. * @param groupManager the group manager * @param newMembers the list of new group members * @throws IOException * @throws ContentDecodingException * @throws ConfigurationException * @throws InvalidKeyException * @throws NoSuchAlgorithmException */ public void setMembershipList(GroupManager groupManager, java.util.Collection<Link> newMembers) throws ContentDecodingException, IOException, InvalidKeyException, NoSuchAlgorithmException { // need to figure out if we need to know private key; if we do and we don't, throw access denied. // We're deleting anyone that exists this._groupManager = groupManager; // TODO don't pull ML twice -- either get it and hand it to modify or let modify get it MembershipListObject ml = membershipList(); // force retrieval if haven't already. if (ml.available() && !ml.isGone() && (ml.membershipList().contents().size() > 0)) { modify(newMembers, ml.membershipList().contents()); } else { modify(newMembers, null); } } /** * Generate a new group public key, e.g. after membership update. * Note that this method does NOT update the public keys of parent and ancestor groups. * To ensure correct recursive updates of the public keys of all ancestor groups, * use instead the public method newGroupPublicKey. * The caller of this method must have access rights to the existing (soon to be previous) * private key of the group. * The new key is created with a call to createGroupPublicKey. This method also wraps * the new private key under the public keys of all the members of the group. * Finally, a superseded block and a link to the previous key are written to the repository. * @param ml the new membership list * @throws IOException * @throws ContentEncodingException * @throws ConfigurationException * @throws InvalidKeyException * @throws NoSuchAlgorithmException */ private void newGroupPublicKeyNonRecursive(MembershipListObject ml) throws ContentEncodingException, IOException, InvalidKeyException, NoSuchAlgorithmException{ PrincipalKeyDirectory oldPrivateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); oldPrivateKeyDirectory.waitForNoUpdates(SystemConfiguration.MEDIUM_TIMEOUT); Key oldPrivateKeyWrappingKey = oldPrivateKeyDirectory.getUnwrappedKey(null); if (null == oldPrivateKeyWrappingKey) { throw new AccessDeniedException("Cannot update group membership, do not have access rights to private key for group " + friendlyName()); }else{ stopPrivateKeyDirectoryEnumeration(); } // Generate key pair // Write public key to new versioned name // Open key directory under that name // Wrap private key in wrapping key, write that block // For each principal on membership list, write wrapped key block Key privateKeyWrappingKey = createGroupPublicKey(ml); // Write superseded block in old key directory oldPrivateKeyDirectory.addSupersededByBlock(oldPrivateKeyWrappingKey, publicKeyName(), null, privateKeyWrappingKey); // Write link back to previous key Link lr = new Link(_groupPublicKey.getVersionedName(), new LinkAuthenticator(new PublisherID(_handle.keyManager().getDefaultKeyID()))); LinkObject precededByBlock = new LinkObject(PrincipalKeyDirectory.getPreviousKeyBlockName(publicKeyName()), lr, SaveType.REPOSITORY, _handle); precededByBlock.save(); } /** * Generate a new group public key, e.g. after membership update. * The caller of this method must have access rights to the existing (soon to be previous) * private key of the group. * The new key is created with a call to createGroupPublicKey. This method also wraps * the new private key under the public keys of all the members of the group. * Finally, a superseded block and a link to the previous key are written to the repository. * @param ml the new membership list * @throws IOException * @throws ContentEncodingException * @throws ConfigurationException * @throws InvalidKeyException * @throws NoSuchAlgorithmException */ public void newGroupPublicKey(MembershipListObject ml) throws ContentEncodingException, IOException, InvalidKeyException, NoSuchAlgorithmException { PrincipalKeyDirectory oldPrivateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); oldPrivateKeyDirectory.waitForChildren(); Key oldPrivateKeyWrappingKey = oldPrivateKeyDirectory.getUnwrappedKey(null); if (null == oldPrivateKeyWrappingKey) { throw new AccessDeniedException("Cannot update group membership, do not have access rights to private key for group " + friendlyName()); } else { stopPrivateKeyDirectoryEnumeration(); } // Generate key pair // Write public key to new versioned name // Open key directory under that name // Wrap private key in wrapping key, write that block // For each principal on membership list, write wrapped key block Key privateKeyWrappingKey = createGroupPublicKey(ml); // Write superseded block in old key directory oldPrivateKeyDirectory.addSupersededByBlock(oldPrivateKeyWrappingKey, publicKeyName(), null, privateKeyWrappingKey); // Write link back to previous key Link lr = new Link(_groupPublicKey.getVersionedName(), new LinkAuthenticator(new PublisherID(_handle.keyManager().getDefaultKeyID()))); LinkObject precededByBlock = new LinkObject(PrincipalKeyDirectory.getPreviousKeyBlockName(publicKeyName()), lr, SaveType.REPOSITORY, _handle); precededByBlock.save(); // generate new public keys for ancestor groups ArrayList<Link> ancestors = recursiveAncestorList(null); Iterator<Link> iter = ancestors.iterator(); while (iter.hasNext()) { Group parentGroup = new Group(iter.next().targetName(), _handle, _groupManager); parentGroup.newGroupPublicKeyNonRecursive(parentGroup.membershipList()); } } /** * Creates a public key for the group, * We don't expect there to be an existing key. So we just write a new one. * If we're not supposed to be a member, this is tricky... we just live * with the fact that we know the private key, and forget it. * @param ml the membership list. * @return the group private key wrapping key. * @throws IOException * @throws ContentEncodingException * @throws ConfigurationException * @throws InvalidKeyException */ public Key createGroupPublicKey(MembershipListObject ml) throws ContentEncodingException, IOException, InvalidKeyException { KeyPairGenerator kpg = null; try { kpg = KeyPairGenerator.getInstance(_groupManager.getGroupKeyAlgorithm()); } catch (NoSuchAlgorithmException e) { if (_groupManager.getGroupKeyAlgorithm().equals(GroupAccessControlManager.DEFAULT_GROUP_KEY_ALGORITHM)) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.SEVERE)) { Log.severe(Log.FAC_ACCESSCONTROL, "Cannot find default group public key algorithm: " + GroupAccessControlManager.DEFAULT_GROUP_KEY_ALGORITHM + ": " + e.getMessage()); } throw new RuntimeException("Cannot find default group public key algorithm: " + GroupAccessControlManager.DEFAULT_GROUP_KEY_ALGORITHM + ": " + e.getMessage()); } throw new InvalidKeyException("Specified group public key algorithm " + _groupManager.getGroupKeyAlgorithm() + " not found. " + e.getMessage()); } kpg.initialize(GroupAccessControlManager.DEFAULT_GROUP_KEY_LENGTH); KeyPair pair = kpg.generateKeyPair(); _groupPublicKey = new PublicKeyObject( GroupAccessControlProfile.groupPublicKeyName(_groupNamespace, _groupFriendlyName), pair.getPublic(), SaveType.REPOSITORY, _handle); _groupPublicKey.save(); _groupPublicKey.updateInBackground(true); stopPrivateKeyDirectoryEnumeration(); _privKeyDirectory = null; PrincipalKeyDirectory newPrivateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); // takes from new public key Key privateKeyWrappingKey = WrappedKey.generateNonceKey(); try { // write the private key newPrivateKeyDirectory.addPrivateKeyBlock(pair.getPrivate(), privateKeyWrappingKey); } catch (InvalidKeyException e) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Unexpected -- InvalidKeyException wrapping key with keys we just generated! " + e.getMessage()); } throw e; } // Wrap the private key in the public keys of all the members of the group updateGroupPublicKey(privateKeyWrappingKey, ml.membershipList().contents()); return privateKeyWrappingKey; } @SuppressWarnings("serial") public static class CouldNotRetrievePublicKeyException extends IOException {} /** * Adds members to an existing group. * The caller of this method must have access to the private key of the group. * We need to wrap the group public key wrapping key in the latest public * keys of the members to add. * Since members are only added, there is no need to replace the group key. * @param privateKeyWrappingKey the private key wrapping key * @param membersToAdd the members added to the group * @throws InvalidKeyException * @throws AccessDeniedException * @throws IOException * @throws ContentDecodingException */ public void updateGroupPublicKey(Key privateKeyWrappingKey, java.util.Collection<Link> membersToAdd) throws InvalidKeyException, ContentDecodingException, AccessDeniedException, IOException { if ((null == membersToAdd) || (membersToAdd.size() == 0)) return; if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINEST)) Log.finest(Log.FAC_ACCESSCONTROL, " {0} Group.updateGroupPublicKey()", _groupNamespace.prefix()); PrincipalKeyDirectory privateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); PublicKeyObject latestPublicKey = null; for (Link lr : membersToAdd) { // DKS TODO verify target public key against publisher, etc in link ContentName mlName = lr.targetName(); ContentName pkName = null; if (_groupManager.getAccessManager().isGroupName(mlName)){ // MLAC mods to make sure we fully parameterize key names pkName = _groupManager.getAccessManager().groupPublicKeyName(mlName); // write a back pointer from child group to parent group // PG TODO check for existence of back pointer to avoid writing multiple copies of the same pointer Link backPointer = new Link(groupName(), friendlyName(), null); ContentName bpNamespace = GroupAccessControlProfile.groupPointerToParentGroupName(lr.targetName()); LinkObject bplo = new LinkObject(new ContentName(bpNamespace, friendlyName()), backPointer, SaveType.REPOSITORY, _handle); bplo.save(); } else { // MLAC mods to make sure we fully parameterize key names pkName = _groupManager.getAccessManager().userPublicKeyName(mlName); } latestPublicKey = new PublicKeyObject(pkName, _handle); if (!latestPublicKey.available()) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Could not retrieve public key for " + pkName); } throw new CouldNotRetrievePublicKeyException(); } // Need to write wrapped key block and linking principal name. try { privateKeyDirectory.addWrappedKeyBlock( privateKeyWrappingKey, latestPublicKey.getVersionedName(), latestPublicKey.publicKey()); } catch (VersionMissingException e) { // TODO make VersionMissingException a subclass of IOException, see case #100070 Log.warningStackTrace(e); throw new IOException(e.toString()); } } } /** * You won't actually get the PrivateKey unles you have the rights to decrypt it; * otherwise you'll get an AccessDeniedException. * @throws IOException * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ public PrivateKey getPrivateKey() throws IOException, InvalidKeyException, NoSuchAlgorithmException { // TODO might do a little unnecessary enumeration, but will pull from cache if in cache. PrincipalKeyDirectory privateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); PrivateKey privateKey = (PrivateKey)privateKeyDirectory.getPrivateKey(); if (null != privateKey) { // Will redundantly re-add to cache. TODO move caching into getPrivateKey; it needs // the public key to do that. _handle.keyManager().getSecureKeyCache().addPrivateKey(privateKeyDirectory.getPrivateKeyBlockName(), publicKeyObject().publicKeyDigest().digest(), privateKey); } return privateKey; } /** * Print useful name and version information. */ @Override public String toString() { StringBuffer sb = new StringBuffer("Group "); sb.append(friendlyName()); sb.append(": public key: "); if (!_groupPublicKey.available()) { sb.append("not ready, write to " + GroupAccessControlProfile.groupPublicKeyName(_groupNamespace, friendlyName())); } else { sb.append(publicKeyName()); } sb.append(" membership list: "); if ((null == _groupMembers) || (!_groupMembers.available())) { sb.append("not ready, will write to " + GroupAccessControlProfile.groupMembershipListName(_groupNamespace, friendlyName())); } else { try { sb.append(membershipListName()); } catch (Exception e) { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) { Log.warning(Log.FAC_ACCESSCONTROL, "Unexpected " + e.getClass().getName() + " exception in getMembershipListName(): " + e.getMessage()); } sb.append("Membership list name unavailable!"); } } return sb.toString(); } /** * Modify will add and remove members from a Group. * It can be used to only add members, in which case the membersToRemove list is null * or it can be used to only remove members, in which case the membersToAdd list is null. * If both lists are passed in, then the items in the membersToAdd list are added and the * items in the membersToRemove are then removed from the Group members list. * * @param membersToAdd list of group members to be added * @param membersToRemove list of group members to be removed * @throws IOException * @throws ContentDecodingException * @throws InvalidKeyException * @throws ConfigurationException * @throws NoSuchAlgorithmException */ public void modify(java.util.Collection<Link> membersToAdd, java.util.Collection<Link> membersToRemove) throws InvalidKeyException, ContentDecodingException, IOException, NoSuchAlgorithmException { if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINEST)) Log.finest(Log.FAC_ACCESSCONTROL, "{0} Group.modify({1},{2})", _groupNamespace.prefix(), (membersToAdd == null) ? "null" : membersToAdd.size(), (membersToRemove == null) ? "null" : membersToRemove.size()); boolean addedMembers = false; boolean removedMembers = false; if (((null == membersToAdd) || (membersToAdd.size() == 0)) && ((null == membersToRemove) || (membersToRemove.size() == 0))) { return; // nothing to do } // You don't want to modify membership list if you dont have permission. // Assume no concurrent writer. PrincipalKeyDirectory privateKeyDirectory = privateKeyDirectory(_groupManager.getAccessManager()); Key privateKeyWrappingKey = privateKeyDirectory.getUnwrappedKey(null); if (null == privateKeyWrappingKey) { throw new AccessDeniedException("Cannot update group membership, do not have acces rights to private key for group " + friendlyName()); }else{ stopPrivateKeyDirectoryEnumeration(); } // Do we need to wait for data to come in? We use this to create new groups as well... // so in that case, don't expect any. // Get the existing membership list, if we don't have it already if (null == _groupMembers) membershipList(); // Add before remove so that remove overrides adds. if ((null != membersToAdd) && (!membersToAdd.isEmpty())) { if (null == _groupMembers.membershipList()) { _groupMembers.setData(new Collection(membersToAdd)); addedMembers = true; } else { // Optimization: check to see if any were already in there before adding them.... addedMembers = _groupMembers.membershipList().contents().addAll(membersToAdd); } } if ((null != membersToRemove) && (!membersToRemove.isEmpty()) && _groupMembers.available() && // do we wait if it's not ready? we know one exists. (!_groupMembers.isGone()) && (_groupMembers.membershipList().contents().size() > 0)) { // There were already members. Remove them and make a new key. removedMembers = _groupMembers.membershipList().contents().removeAll(membersToRemove); } if (removedMembers) { // Don't save membership list till we know we can update private key. // If we can't update the private key, this will throw AccessDeniedException. newGroupPublicKey(_groupMembers); } else if (addedMembers) { // additions only. Don't have to make a new key if one exists, // just rewrap it for added members. if (null != _groupPublicKey.publicKey()) { updateGroupPublicKey(privateKeyWrappingKey, membersToAdd); } else { createGroupPublicKey(_groupMembers); } } // Don't actually save the new membership list till we're sure we can update the // key. _groupMembers.save(); } public void delete() throws IOException { // Deleting the group -- mark both membership list and public key as GONE. _groupMembers.saveAsGone(); _groupPublicKey.saveAsGone(); } /** * Recursively constructs an ordered list of the ancestors of the group. * The ancestors are the groups of which the group is a member either directly * or indirectly via a chain of one or more ancestors. * The order ensures that a group is always listed after all its children. * @param ancestorList the ancestor list built up to this point * @return the recursively updated ancestor list * @throws IOException */ public ArrayList<Link> recursiveAncestorList(ArrayList<Link> ancestorList) throws IOException { if (ancestorList == null) ancestorList = new ArrayList<Link>(); ContentName cn = GroupAccessControlProfile.groupPointerToParentGroupName(groupName()); EnumeratedNameList parentList = new EnumeratedNameList(cn, _handle); parentList.waitForChildren(PARENT_GROUP_ENUMERATION_TIMEOUT); if (parentList.hasChildren()) { SortedSet<ContentName> parents = parentList.getChildren(); for (ContentName parentLinkName : parents) { ContentName pln = new ContentName(cn, parentLinkName.component(0)); LinkObject parentLinkObject = new LinkObject(pln, _handle); Link parentLink = parentLinkObject.link(); // delete the link if already present in the list and re-insert it at the end of the list if (ancestorList.contains(parentLink)) ancestorList.remove(parentLink); ancestorList.add(parentLink); Group parentGroup = new Group(parentLink.targetName(), _handle, _groupManager); parentGroup.recursiveAncestorList(ancestorList); } } parentList.stopEnumerating(); return ancestorList; } }