/*
* 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.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
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.CCNReader;
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.ContentNotReadyException;
import org.ccnx.ccn.io.content.Link;
import org.ccnx.ccn.io.content.PublicKeyObject;
import org.ccnx.ccn.profiles.nameenum.EnumeratedNameList;
import org.ccnx.ccn.profiles.namespace.ParameterizedName;
import org.ccnx.ccn.profiles.security.access.group.GroupAccessControlProfile.PrincipalInfo;
import org.ccnx.ccn.protocol.CCNTime;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.PublisherID;
/**
* A meta-class for group management, handling in particular:
* - Creation of new groups
* - Retrieval of existing groups
* - Determination of group membership
* - Retrieval of group private and public keys
*
* There is currently one GroupManager per Group namespace. If you haven't loaded the GroupManager
* for a given namespace, and attempt to write ACLs naming groups defined in that namespace, you will
* get errors as the access control code won't recognise those entitites as Groups or be able to
* find their public keys.
*/
public class GroupManager {
private GroupAccessControlManager _accessManager;
private ParameterizedName _groupStorage;
private EnumeratedNameList _groupList;
private HashMap<String, Group> _groupCache = new HashMap<String, Group>();
private HashSet<String> _myGroupMemberships = new HashSet<String>();
private CCNHandle _handle;
public GroupManager(GroupAccessControlManager accessManager,
ParameterizedName groupStorage, CCNHandle handle) {
_handle = handle;
_accessManager = accessManager;
_groupStorage = groupStorage;
}
/**
* A "quiet" constructor that doesn't enumerate anything, and in fact does
* little to be used for non-group based uses of KeyDirectory, really
* a temporary hack till we refactor KD.
* @return
*/
GroupManager(GroupAccessControlManager accessManager, CCNHandle handle) throws IOException {
_handle = handle;
_accessManager = accessManager;
_groupStorage = null; // try this, see if it explodes
}
public GroupAccessControlManager getAccessManager() { return _accessManager; }
/**
* Enumerate groups
* @return the enumeration of groups
* @throws IOException
*/
public EnumeratedNameList groupList() throws IOException {
if (null == _groupList) {
_groupList = new EnumeratedNameList(_groupStorage.prefix(), _handle);
}
return _groupList;
}
/**
* Get a group specified by its friendly name
* @param groupFriendlyName the friendly name of the group
* @return the corresponding group
* @throws IOException
* @throws ContentDecodingException
*/
public Group getGroup(String groupFriendlyName, long timeout) throws ContentDecodingException, IOException {
if ((null == groupFriendlyName) || (groupFriendlyName.length() == 0)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Asked to retrieve group with empty name.");
}
return null;
}
Group theGroup = _groupCache.get(groupFriendlyName);
// Need to wait for data and add time out.
// The first time you run this, nothing will be read.
/*if( null == theGroup) {
groupList().waitForData();
SortedSet<ContentName> children = groupList().getChildren();
System.out.println("found the following groups:.................");
for (ContentName child : children) {
System.out.println(child);
}
}*/
if (null == theGroup) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "The group {0} was not found in the group cache.", groupFriendlyName);
}
synchronized(_groupCache) {
theGroup = _groupCache.get(groupFriendlyName);
if (null == theGroup) {
// Only go hunting for it if we think it exists, otherwise we'll block.
if (groupExists(groupFriendlyName, timeout)) {
theGroup = new Group(_groupStorage, groupFriendlyName, _handle, this);
// wait for group to be ready?
_groupCache.put(groupFriendlyName, theGroup);
}
}
}
}
// either we've got it, or we don't believe it exists.
// startup transients? do we need to block for group list?
return theGroup;
}
/**
* Get the group specified by a link
* @param theGroup link to the group
* @return the corresponding group
* @throws IOException
* @throws ContentDecodingException
*/
public Group getGroup(Link theGroup, long timeout) throws ContentDecodingException, IOException {
if (null == theGroup) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Asked to retrieve group with empty link.");
}
return null;
}
if (!isGroup(theGroup))
return null;
String friendlyName = GroupAccessControlProfile.groupNameToFriendlyName(theGroup.targetName());
return getGroup(friendlyName, timeout);
}
/**
* Replace enumeration-based test of existence with direct test.
* @throws IOException
*/
public boolean groupExists(String groupFriendlyName, long timeout) throws IOException {
ContentName publicKeyName =
GroupAccessControlProfile.groupPublicKeyName(_groupStorage, groupFriendlyName);
// Take any content below the public key name -- key fragments, keys, whatever's fastest.
// This will take a long time if the group doesn't exist, but should be fast if it does,
// with no pre-enumeration required.
return (null != CCNReader.isAnyContentAvailable(publicKeyName, null, timeout, _handle));
}
/**
* Adds the specified group to the cache
* @param newGroup the group
*/
public void cacheGroup(Group newGroup) {
synchronized(_groupCache) {
_groupCache.put(newGroup.friendlyName(), newGroup);
}
}
/**
* Create a new group with a specified friendly name and list of members
* The creator of the group ends up knowing the private key of the newly created group
* but is simply assumed to forget it if not a member.
* @param groupFriendlyName the friendly name of the group
* @param newMembers the members of the group
* @return the group
* @throws IOException
* @throws ConfigurationException
* @throws ContentEncodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public Group createGroup(String groupFriendlyName, ArrayList<Link> newMembers, long timeToWaitForPreexisting)
throws InvalidKeyException, ContentEncodingException, IOException, NoSuchAlgorithmException {
Group existingGroup = null;
if (timeToWaitForPreexisting > 0) {
existingGroup = getGroup(groupFriendlyName, timeToWaitForPreexisting);
}
if (null != existingGroup) {
existingGroup.setMembershipList(this, newMembers);
return existingGroup;
} else {
// Need to make key pair, directory, and store membership list.
MembershipListObject ml =
new MembershipListObject(
GroupAccessControlProfile.groupMembershipListName(_groupStorage, groupFriendlyName),
new Collection(newMembers), SaveType.REPOSITORY, _handle);
Group newGroup = new Group(_groupStorage, groupFriendlyName, ml, _handle, this);
cacheGroup(newGroup);
if (amCurrentGroupMember(newGroup)) {
_myGroupMemberships.add(groupFriendlyName);
}
return newGroup;
}
}
/**
* Delete an existing group specified by its friendly name.
* @param friendlyName the friendly name of the group
* @throws IOException
* @throws ContentDecodingException
*/
public void deleteGroup(String friendlyName) throws ContentDecodingException, IOException {
Group existingGroup = getGroup(friendlyName, SystemConfiguration.EXTRA_LONG_TIMEOUT);
// We really want to be sure we get the group if it's out there...
if (null != existingGroup) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Got existing group to delete: {0}", existingGroup);
}
existingGroup.delete();
} else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.WARNING)) {
Log.warning(Log.FAC_ACCESSCONTROL, "No existing group: {0}, ignoring delete request.", friendlyName);
}
}
}
/**
* Does this member refer to a user or a group. Groups have to be in the
* group namespace, users can be anywhere.
* @param member
* @return
*/
public boolean isGroup(Link member) {
return _groupStorage.prefix().isPrefixOf(member.targetName());
}
public boolean isGroup(String principal, long timeout) throws IOException {
return (null != getGroup(principal, timeout));
}
public boolean isGroup(ContentName publicKeyName) {
return _groupStorage.prefix().isPrefixOf(publicKeyName);
}
public boolean haveKnownGroupMemberships() {
return _myGroupMemberships.size() > 0;
}
public boolean amKnownGroupMember(String principal) {
return _myGroupMemberships.contains(principal);
}
public boolean amCurrentGroupMember(String principal) throws ContentDecodingException, IOException {
return amCurrentGroupMember(getGroup(principal, SystemConfiguration.EXTRA_LONG_TIMEOUT));
}
/**
* Determine if I am a current group member of a specified group.
* The current implementation of this method is slow and simple.
* It can be optimized later.
* @param group the group
* @return
* @throws IOException
* @throws ContentDecodingException
*/
public boolean amCurrentGroupMember(Group group) throws ContentDecodingException, IOException {
MembershipListObject ml = group.membershipList(); // will update
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "amCurrentGroupMember: group {0} has {1} member(s).", group.groupName(), ml.membershipList().size());
}
for (Link lr : ml.membershipList().contents()) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "amCurrentGroupMember: {0} is a member of group {1}", lr.targetName(), group.groupName());
}
GroupManager gm = _accessManager.groupManager(lr.targetName().parent());
if (gm != null && gm.isGroup(lr)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "amCurrentGroupMember: {0} is itself a group.", lr.targetName());
}
String groupFriendlyName = GroupAccessControlProfile.groupNameToFriendlyName(lr.targetName());
if (gm.amCurrentGroupMember(groupFriendlyName)) {
_myGroupMemberships.add(groupFriendlyName);
return true;
} else {
// Don't need to test first. Won't remove if isn't there.
_myGroupMemberships.remove(groupFriendlyName);
}
} else {
// Not a group. Is it me?
if (_accessManager.haveIdentity(lr.targetName())) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "amCurrentGroupMember: {0} is me!", lr.targetName());
}
_myGroupMemberships.add(group.friendlyName());
return true;
}
else {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.FINER)) {
Log.finer(Log.FAC_ACCESSCONTROL, "amCurrentGroupMember: {0} is not me.", lr.targetName());
}
}
}
}
return false;
}
/**
* Get the private key of a group specified by its friendly name.
* I already believe I should have access to this private key.
* @param groupFriendlyName the group friendly name
* @param privateKeyVersion the version of the private key
* @return the group private key
* @throws IOException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
public Key getGroupPrivateKey(String groupFriendlyName, CCNTime privateKeyVersion)
throws ContentDecodingException, IOException, InvalidKeyException, NoSuchAlgorithmException {
// Heuristic check
if (!amKnownGroupMember(groupFriendlyName)) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Unexpected: we don't think we're a group member of group " + groupFriendlyName);
}
}
// Need to get the KeyDirectory for this version of the private key, or the
// latest if no version given.
PrincipalKeyDirectory privateKeyDirectory;
PublicKey theGroupPublicKey = null;
if (null == privateKeyVersion) {
Group theGroup = getGroup(groupFriendlyName, SystemConfiguration.EXTRA_LONG_TIMEOUT); // will pull latest public key
privateKeyDirectory = theGroup.privateKeyDirectory(_accessManager);
privateKeyDirectory.waitForNoUpdatesOrResult(SystemConfiguration.SHORT_TIMEOUT);
theGroupPublicKey = theGroup.publicKey();
} else {
// Assume one is there...
ContentName versionedPublicKeyName =
new ContentName(
GroupAccessControlProfile.groupPublicKeyName(_groupStorage, groupFriendlyName),
privateKeyVersion);
privateKeyDirectory =
new PrincipalKeyDirectory(_accessManager,
GroupAccessControlProfile.groupPrivateKeyDirectory(versionedPublicKeyName), _handle);
privateKeyDirectory.waitForNoUpdatesOrResult(SystemConfiguration.SHORT_TIMEOUT);
PublicKeyObject thisPublicKey = new PublicKeyObject(versionedPublicKeyName, _handle);
thisPublicKey.waitForData();
theGroupPublicKey = thisPublicKey.publicKey();
}
Key privateKey = privateKeyDirectory.getPrivateKey();
if (null != privateKey && (privateKey instanceof PrivateKey)) {
_handle.keyManager().getSecureKeyCache().addPrivateKey(privateKeyDirectory.getPrivateKeyBlockName(),
PublisherID.generatePublicKeyDigest(theGroupPublicKey), (PrivateKey)privateKey);
}
return privateKey;
}
/**
* We might or might not still be a member of this group, or be a member
* again. This merely removes our cached notion that we are a member.
* @param principal
*/
public void removeGroupMembership(String principal) {
_myGroupMemberships.remove(principal);
}
/**
* Get the algorithm of the group key.
* Eventually let namespace control this.
* @return the algorithm of the group key
*/
public String getGroupKeyAlgorithm() {
return GroupAccessControlManager.DEFAULT_GROUP_KEY_ALGORITHM;
}
/**
* Get the parameterized Name used by this group manager
* @return the parameterized name for the group storage location
*/
public ParameterizedName getGroupStorage() {
return _groupStorage;
}
/**
* Get the versioned private key for a group.
* @param keyDirectory the key directory associated with the group
* @param principal the principal
* @return the versioned private key
* @throws IOException
* @throws ContentNotReadyException
* @throws ContentDecodingException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
*/
protected Key getVersionedPrivateKeyForGroup(PrincipalInfo pi)
throws InvalidKeyException, ContentNotReadyException, ContentDecodingException,
IOException, NoSuchAlgorithmException {
String principal = pi.friendlyName();
Key privateKey = null;
try {
privateKey = getGroupPrivateKey(principal, pi.versionTimestamp());
} catch (PrincipalKeyDirectory.NoPrivateKeyException e) {
if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) {
Log.info(Log.FAC_ACCESSCONTROL, "Unexpected: we believe we are a member of group {0} but cannot retrieve private key version {1} our membership revoked?",
principal, pi);
}
// Check to see if we are a current member.
if (!amCurrentGroupMember(principal)) {
// Removes this group from my list of known groups, adds it to my
// list of groups I don't believe I'm a member of.
removeGroupMembership(principal);
}
}
return privateKey;
}
/**
* Get the latest public key for a group specified by its principal name
* @param principal
* @return
* @throws IOException
* @throws ContentDecodingException
*/
public PublicKeyObject getLatestPublicKeyForGroup(Link principal) throws ContentDecodingException, IOException {
Group theGroup = getGroup(principal, SystemConfiguration.EXTRA_LONG_TIMEOUT);
if (null == theGroup)
return null;
return theGroup.publicKeyObject();
}
}