/* * Part of the CCNx Java Library. * * Copyright (C) 2008, 2009 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.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.TreeMap; import java.util.TreeSet; import java.util.logging.Level; import org.ccnx.ccn.CCNHandle; import org.ccnx.ccn.config.ConfigurationException; import org.ccnx.ccn.impl.CCNFlowControl; import org.ccnx.ccn.impl.CCNFlowControl.SaveType; import org.ccnx.ccn.impl.encoding.CCNProtocolDTags; import org.ccnx.ccn.impl.support.Log; import org.ccnx.ccn.io.ErrorStateException; import org.ccnx.ccn.io.content.CCNEncodableObject; import org.ccnx.ccn.io.content.Collection; import org.ccnx.ccn.io.content.ContentDecodingException; 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.profiles.VersioningProfile; import org.ccnx.ccn.protocol.ContentName; import org.ccnx.ccn.protocol.ContentObject; import org.ccnx.ccn.protocol.KeyLocator; import org.ccnx.ccn.protocol.PublisherPublicKeyDigest; /** * This class represents an Access Control List (ACLs) for CCN content, for use with * the Group-based access control scheme (though it might be useful to other schemes as well). * * It offers a limited degree of expressibility -- it can grant read, write, or manage * privileges to named users or groups (where users and groups are effectively * public keys stored in locations defined by the profile). Permissions are supersets * of one another -- writers can read, managers can read and write. Managers have the additional * capability to change rights -- to create and edit ACLs. An ACL applies to all the content * below it in the name tree until it is superseded by another ACL below it in that tree. * */ public class ACL extends Collection { /** Readers can read content */ public static final String LABEL_READER = "r"; /** Writers can read and write (or edit) content */ public static final String LABEL_WRITER = "rw"; /** Managers can read and write content, and edit access rights to content */ public static final String LABEL_MANAGER = "rw+"; public static final String [] ROLE_LABELS = {LABEL_READER, LABEL_WRITER, LABEL_MANAGER}; /** * This class represents the operations that can be performed on an ACL, * such as add or delete readers, writers or managers. * */ public static class ACLOperation extends Link { public static final String LABEL_ADD_READER = "+r"; public static final String LABEL_ADD_WRITER = "+rw"; public static final String LABEL_ADD_MANAGER = "+rw+"; public static final String LABEL_DEL_READER = "-r"; public static final String LABEL_DEL_WRITER = "-rw"; public static final String LABEL_DEL_MANAGER = "-rw+"; public ACLOperation(String label, Link linkRef){ super(linkRef.targetName(), label, linkRef.targetAuthenticator()); } public static ACLOperation addReaderOperation(Link linkRef){ return new ACLOperation(LABEL_ADD_READER, linkRef); } public static ACLOperation removeReaderOperation(Link linkRef){ return new ACLOperation(LABEL_DEL_READER, linkRef); } public static ACLOperation addWriterOperation(Link linkRef){ return new ACLOperation(LABEL_ADD_WRITER, linkRef); } public static ACLOperation removeWriterOperation(Link linkRef){ return new ACLOperation(LABEL_DEL_WRITER, linkRef); } public static ACLOperation addManagerOperation(Link linkRef){ return new ACLOperation(LABEL_ADD_MANAGER, linkRef); } public static ACLOperation removeManagerOperation(Link linkRef){ return new ACLOperation(LABEL_DEL_MANAGER, linkRef); } // In case anyone tries to serialize. NOT IN SCHEMA. Not supposed to be serialized. @Override public long getElementLabel() { return -1; } } /** * This class is for matching on unversioned link target name only, * not label and potentially not signer if specified. Use a set class that can * allow us to specify a comparator; use one that ignores labels and versions on names. * */ public static class SuperficialLinkComparator implements Comparator<Link> { /** * Compare two links * @param o1 first link * @param o2 second link * @return result of comparison */ public int compare(Link o1, Link o2) { int result = 0; if (null != o1) { if (null == o2) { return 1; } } else if (null != o2) { return -1; } else { return 0; } // Want an ordering on un-versioned names, not a comparison of versions. result = VersioningProfile.cutTerminalVersion(o1.targetName()).first().compareTo(VersioningProfile.cutTerminalVersion(o2.targetName()).first()); if (result != 0) return result; if (null != o1.targetAuthenticator()) { if (null != o2.targetAuthenticator()) { return o1.targetAuthenticator().compareTo(o2.targetAuthenticator()); } else { return 1; } } else if (null != o2.targetAuthenticator()) { return -1; } else { return 0; } } } static SuperficialLinkComparator _comparator = new SuperficialLinkComparator(); protected TreeSet<Link> _readers = new TreeSet<Link>(_comparator); protected TreeSet<Link> _writers = new TreeSet<Link>(_comparator); protected TreeSet<Link> _managers = new TreeSet<Link>(_comparator); /** * ACL CCN objects; as it only makes sense right now to * operate on ACLs in repositories, it writes all data to repositories.. * */ public static class ACLObject extends CCNEncodableObject<ACL> { /** * Constructor * @param name the object name * @param data the ACL * @param handle the CCN handle * @throws ConfigurationException * @throws IOException */ public ACLObject(ContentName name, ACL data, CCNHandle handle) throws IOException { super(ACL.class, true, name, data, SaveType.REPOSITORY, handle); } public ACLObject(ContentName name, ACL data, PublisherPublicKeyDigest publisher, KeyLocator keyLocator, CCNHandle handle) throws IOException { super(ACL.class, true, name, data, SaveType.REPOSITORY, publisher, keyLocator, handle); } /** * Read constructor -- opens existing object. * @param name the object name * @param handle the CCN handle * @throws IOException * @throws ContentDecodingException */ public ACLObject(ContentName name, CCNHandle handle) throws ContentDecodingException, IOException { super(ACL.class, true, name, (PublisherPublicKeyDigest)null, handle); setSaveType(SaveType.REPOSITORY); } /** * Read constructor * @param name the object name * @param publisher the required publisher * @param handle the CCN handle * @throws IOException * @throws ContentDecodingException */ public ACLObject(ContentName name, PublisherPublicKeyDigest publisher, CCNHandle handle) throws ContentDecodingException, IOException { super(ACL.class, true, name, publisher, handle); setSaveType(SaveType.REPOSITORY); } public ACLObject(ContentObject firstBlock, CCNHandle handle) throws ContentDecodingException, IOException { super(ACL.class, true, firstBlock, handle); setSaveType(SaveType.REPOSITORY); } public ACLObject(ContentName name, PublisherPublicKeyDigest publisher, CCNFlowControl flowControl) throws ContentDecodingException, IOException { super(ACL.class, true, name, publisher, flowControl); } public ACLObject(ContentObject firstBlock, CCNFlowControl flowControl) throws ContentDecodingException, IOException { super(ACL.class, true, firstBlock, flowControl); } public ACLObject(ContentName name, ACL data, PublisherPublicKeyDigest publisher, KeyLocator keyLocator, CCNFlowControl flowControl) throws IOException { super(ACL.class, true, name, data, publisher, keyLocator, flowControl); } public ACL acl() throws ContentNotReadyException, ContentGoneException, ErrorStateException { return data(); } } public ACL() { super(); } /** * Constructor * @param contents the contents of the ACL */ public ACL(ArrayList<Link> contents) { if (validate()) add(contents); else throw new IllegalArgumentException("Invalid contents for ACL."); } /** * Return whether an ACL element is valid * @param lr the element * @return */ public boolean validLabel(Link lr) { return LABEL_MANAGER.contains(lr.targetLabel()); } /** * Placeholder for public content. These will be represented by some * form of marker entry, and need to be handled specially. * @return */ public boolean publiclyReadable() { return false; } /** * Placeholder for public content. These will be represented by some * form of marker entry, and need to be handled specially. * @return */ public boolean publiclyWritable() { return false; } /** * Return whether an ACL is valid * @return */ @Override public boolean validate() { if (!super.validate()) return false; for (Link lr : contents()) { if ((null == lr.targetLabel()) || (!validLabel(lr))) { return false; } } return true; } /** * Add a specified reader to the ACL. * The method does nothing if the reader is already a reader, a writer or a manager. * @param reader the reader */ public void addReader(Link reader) { // add the reader only if it's not already a reader, a writer or a manager. if ((! _readers.contains(reader)) && (! _writers.contains(reader)) && (! _managers.contains(reader))) { if (!LABEL_READER.equals(reader.targetLabel())) { reader = new Link(reader.targetName(), LABEL_READER, reader.targetAuthenticator()); } super.add(reader); _readers.add(reader); } } /** * Remove a specified reader from the ACL. * @param reader the reader */ public boolean removeReader(Link reader) { if (!LABEL_READER.equals(reader.targetLabel())) { reader = new Link(reader.targetName(), LABEL_READER, reader.targetAuthenticator()); } if (_readers.contains(reader)) { _contents.remove(reader); _readers.remove(reader); return true; } if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "trying to remove a non-existent reader, ignoring this operation..."); } return false; } /** * Add a specified writer to the ACL. * The method does nothing if the writer is already a writer or a manager. * If the writer is already a reader, it is deleted from _readers and added to _writers. * @param writer the writer */ public void addWriter(Link writer) { // add the writer only if it's not already a writer or a manager. if ((! _writers.contains(writer)) && (! _managers.contains(writer))) { if (!LABEL_WRITER.equals(writer.targetLabel())) { writer = new Link(writer.targetName(), LABEL_WRITER, writer.targetAuthenticator()); } // if the writer is already a reader, delete it from readers. if (_readers.contains(writer)) { // TODO: this will not work if link has different authenticator removeReader(writer); } // add the writer as a writer super.add(writer); _writers.add(writer); } } /** * Remove a specified writer from the ACL. * @param writer the writer */ public boolean removeWriter(Link writer) { if (!LABEL_WRITER.equals(writer.targetLabel())) { writer = new Link(writer.targetName(), LABEL_WRITER, writer.targetAuthenticator()); } if (_writers.contains(writer)) { _contents.remove(writer); _writers.remove(writer); return true; } if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "trying to remove a non-existent writer, ignoring this operation..."); } return false; } /** * Add a specified manager to the ACL * This method does nothing if the manager is already a manager. * If the manager is already a reader or a writer, it is removed from * _readers or _writers and added to _managers. * @param manager the manager */ public void addManager(Link manager) { // add the manager only if it's not already a manager. if (! _managers.contains(manager)) { if (!LABEL_MANAGER.equals(manager.targetLabel())) { manager = new Link(manager.targetName(), LABEL_MANAGER, manager.targetAuthenticator()); } // if the manager is already a reader, delete it from readers. if (_readers.contains(manager)) { // TODO: this will not work if link has different authenticator removeReader(manager); } // if the manager is already a writer, delete it from readers. else if (_writers.contains(manager)) { // TODO: this will not work if link has different authenticator removeWriter(manager); } // add the manager as a manager super.add(manager); _managers.add(manager); } } /** * Remove a specified manager from the ACL. * @param manager the manager */ public boolean removeManager(Link manager) { if (!LABEL_MANAGER.equals(manager.targetLabel())) { manager = new Link(manager.targetName(), LABEL_MANAGER, manager.targetAuthenticator()); } if (_managers.contains(manager)) { _contents.remove(manager); _managers.remove(manager); return true; } if (Log.isLoggable(Log.FAC_ACCESSCONTROL, Level.INFO)) { Log.info(Log.FAC_ACCESSCONTROL, "trying to remove a non-existent manager, ignoring this operation..."); } return false; } /** * Batch perform a set of ACL update Operations * @param ACLUpdates: ordered set of ACL update operations * @return We return a LinkedList<Link> of the principals newly granted read * access on this ACL. If no individuals are granted read access, we return a 0-length * LinkedList. If any individuals are completely removed, requiring the caller to generate * a new node key or otherwise update cryptographic data, we return null. * (We could return the removed principals, but it's a little weird -- some people are * removed from a role and added to others. For now, we just return the thing we need * for our current implementation, which is whether anyone lost read access entirely.) */ public LinkedList<Link> update(ArrayList<ACLOperation> ACLUpdates){ final int LEVEL_NONE = 0; final int LEVEL_READ = 1; final int LEVEL_WRITE = 2; final int LEVEL_MANAGE = 3; //for principals that are affected, //tm records the previous privileges of those principals TreeMap<Link, Integer> tm = new TreeMap<Link, Integer>(_comparator); for (ACLOperation op: ACLUpdates) { int levelOld = LEVEL_NONE; if (_readers.contains(op)) { levelOld = LEVEL_READ; } else if (_writers.contains(op)) { levelOld = LEVEL_WRITE; } else if (_managers.contains(op)){ levelOld = LEVEL_MANAGE; } if (ACLOperation.LABEL_ADD_READER.equals(op.targetLabel())) { addReader(op); } else if (ACLOperation.LABEL_ADD_WRITER.equals(op.targetLabel())) { addWriter(op); } else if (ACLOperation.LABEL_ADD_MANAGER.equals(op.targetLabel())) { addManager(op); } else if (ACLOperation.LABEL_DEL_READER.equals(op.targetLabel())) { removeReader(op); } else if (ACLOperation.LABEL_DEL_WRITER.equals(op.targetLabel())){ removeWriter(op); } else if (ACLOperation.LABEL_DEL_MANAGER.equals(op.targetLabel())) { removeManager(op); } if (!tm.containsKey(op)) { tm.put(op, levelOld); } } // a new node key is required if someone with LEVEL_READ or above // is down-graded to LEVEL_NONE boolean newKeyRequired = false; LinkedList<Link> newReaders = new LinkedList<Link>(); Iterator<Link> it = tm.keySet().iterator(); while (it.hasNext()) { Link p = it.next(); int lvOld = tm.get(p); if (_readers.contains(p) || _writers.contains(p) || _managers.contains(p)) { if (lvOld == LEVEL_NONE) { newReaders.add(p); } } else if (lvOld > LEVEL_NONE) { newKeyRequired = true; } } if (newKeyRequired) { return null; } return newReaders; } @Override public void add(Link link) { String label = link.targetLabel(); if (label.equals(LABEL_READER)) addReader(link); else if (label.equals(LABEL_WRITER)) addWriter(link); else if (label.equals(LABEL_MANAGER)) addManager(link); else throw new IllegalArgumentException("Invalid ACL label: " + link.targetLabel()); } @Override public void add(ArrayList<Link> contents) { for (Link link : contents) { add(link); // break them out for validation and indexing } } @Override public Link remove(int i) { Link link = _contents.get(i); remove(link); return link; } @Override public boolean remove(Link content) { String label = content.targetLabel(); if (label.equals(LABEL_READER)) return removeReader(content); else if (label.equals(LABEL_WRITER)) return removeWriter(content); else if (label.equals(LABEL_MANAGER)) return removeManager(content); return false; } @Override public void removeAll() { super.removeAll(); _readers.clear(); _writers.clear(); _managers.clear(); } @Override public long getElementLabel() { return CCNProtocolDTags.ACL; } }