diff --git a/COMMITTERS b/COMMITTERS
new file mode 100644
index 00000000..cad7a5a1
--- /dev/null
+++ b/COMMITTERS
@@ -0,0 +1,31 @@
+The following people have commit access to the Sakai Project fork
+of the Sparse Map Content System sources originally authored
+by Ian Boston (ian@tfd.co.uk). Note that this is not a
+full list of the authors; for that, you will need to look
+over the log messages to see all the patch contributors.
+
+Committers:
+
+ carl@hallwaytech.com Carl Hall
+ zach@aeroplanesoftware.com Zach Thomas
+ chris@media.berkeley.edu Chris Tweney
+ arwhyte@umich.edu Anthony Whyte
+
+Contributors:
+
+For a complete list of contributions please see the commit log.
+Contributors include:
+
+ ian@tfd.co.uk Ian Boston (original author)
+ ray@media.berkeley.edu Ray Davis
+ cdunstall@csu.edu.au Chris Dunstall
+ erik.froese@gmail.com Erik Froese
+ duffy@rsmart.com Duffy Gillman
+ johnk@media.berkeley.edu John King
+ kotwal.aadish@gmail.com Aadish Kotwal
+ droma@csu.edu.au Dave Roma
+ mark@dishevelled.net Mark Triggs
+ mawalsh@csu.edu.au Mark Walsh
+ roberttdev@gmail.com Rob Williams
+
+
diff --git a/CONTRIBUTING b/CONTRIBUTING
new file mode 100644
index 00000000..dcc9a57d
--- /dev/null
+++ b/CONTRIBUTING
@@ -0,0 +1,9 @@
+Please read the README and NOTICE files before contributing.
+Contributions are very welcome under those terms, but its your responsibility
+to ensure that the contributions meet those requirements.
+
+All patches prior to this file appearing in the code base were contributed under no
+explicit policy on accepting patches and Timefields Ltd holds no copyright to those
+contributons and makes no assertions as to the IPR status or patent status of those
+contributions.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..75b52484
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 00000000..9ccf1b96
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,18 @@
+Sparse Content Bundle
+Copyright 2011 Timefields Ltd
+
+The Copyright of patches appearing in the code base prior to the appearance of this NOTICE and CONTRIBUTING file
+were accepted under the assumption that re-licensing of those patches, under the Apache 2 Software License to the
+Sakai Foundation was acceptable to the contributors, and that there was nothing in the patches that would prevent
+that from happening, infringe on any patents or IPR. If you are concerned about this, you can find authors using
+the version control system.
+
+-----------------------------------------------------------
+
+This product includes software from the The Apache Software Foundation (http://www.apache.org/).
+
+Patches and contributions made to this code base are made under the terms of the Apache 2 Software License (see para 5).
+
+Binary distributions of this product contain jars developed and licensed by other third parties, identified by the
+LICENSE and NOTICE files included within each jar under the META-INF directory.
+
diff --git a/README.textile b/README.textile
index 087225d1..31ea7e74 100644
--- a/README.textile
+++ b/README.textile
@@ -1,35 +1,18 @@
h1. Map Content System.
-h2. Rational
+h2. Rationale
- In the Q1 release of Nakamura we had major scalability and concurrency problems caused mainly by our use cases for a content
-store not being closely aligned with those of Jackrabbit. We were not able to work around those problems and although we did manage
-to release the code, its quite clear that in certain areas Jackrabbit wont work for us. This should not reflect badly on Jackrabbit,
-but it is a realization that our use cases are not compatible with Jackrabbit when exposed to scale.
+In the Q1 release of Nakamura we had major scalability and concurrency problems caused mainly by our use cases for a content store not being closely aligned with those of Jackrabbit. We were not able to work around those problems and although we did manage to release the code, its quite clear that in certain areas Jackrabbit won't work for us. This should not reflect badly on Jackrabbit, but it is a realization that our use cases are not compatible with Jackrabbit when exposed to scale.
- This code base is a reaction to that. It aims to be really simple, completely concurrent with no synchronization and designed to scale
-linearly with the number of cores and number of servers in a cluster. To do this it borrows some of the concepts from JCR at a very
-abstract level, but is making a positive effort and selfish effort to only provide those things that we absolutely need to have.
+This code base is a reaction to that. It aims to be really simple, completely concurrent with no synchronization and designed to scale linearly with the number of cores and number of servers in a cluster. To do this it borrows some of the concepts from JCR at a very abstract level, but is making a positive effort and selfish effort to only provide those things that we absolutely need to have.
- This code provides User, Group, Access Control and Content functionality using a sparse Map as a storage abstraction.
+This code provides User, Group, Access Control and Content functionality using a sparse Map as a storage abstraction. The implementation works on manipulating sparse objects in the Map with operations like get, insert and delete, but has no understanding of the underlying implementation of the storage mechanism.
- The Implementation works on manipulating sparse objects in the Map with operations like get, insert and delete, but
-has no understanding of the underlying implementation of the storage mechanism.
+At the moment we have 3 storage mechanisms implemented, In Memory using a HashMap, Cassandra and JDBC capable of doing sharded storage, The approach should work on any Column Store (Dynamo, BigTable, Riak, Voldomort, Hbase etc). The JDBC Driver has configuration files for Derby, MySQL, Oracle, PostgreSQL.
- At the moment we have 2 storage mechanisms implemented, In Memory using a HashMap, and Cassandra. The approach should
-work on any Column Store (Dynamo, BigTable, Riak, Voldomort, Hbase etc) and can also work on RDBMS's including sharded storage.
-
- At the moment there is no query support, expecting all access to be via column IDs, and multiple views to be written to the
-underlying store.
-
- The intention is to provide write through caches based on EhCache or Infinispan.
-
- Transactions are supported, if supported by the underlying implementation of the storage, otherwise all operations are BASIC, non Atomic and immediate in nature.
-We will add search indexes at some point using Lucene, perhaps in the form of Zoie
-
-
- At this stage its pre-alpha, untested for performance and scalability and incomplete.
+Query support is provided by finder messages that use index table written on update. Caching support is via an interface allowing external providers. In Nakamura there is an EhCache implementation and it would be a relatively simple task to write an Infinispan version. Transactions are supported, if supported by the underlying implementation of the storage, otherwise all operations are BASIC, non Atomic and immediate in nature.
+Search is provided in the form of a companion project that uses SolrJ 4.
h2. Backlog
@@ -48,17 +31,13 @@ h2. Completed Backlog
# Implement SparseMapUserManager and related classes in th server bundle in Sling. (done 28/11/2010)
-
-
-
h2. Tests
h3. Memory
All performed on a MackBook Pro which is believed to have 4 cores.
-Add a user, 1 - 10 threads. Storage is a Concurrent Hash Map. Assuming the Concurrent Hash Map is 100% concurrent, this test
-tests the code base for concurrent efficiency.
+Add a user, 1 - 10 threads. Storage is a Concurrent Hash Map. Assuming the Concurrent Hash Map is 100% concurrent, this test tests the code base for concurrent efficiency.
|Threads|Time(s)|Throughput|Throughput per thread|Speedup|Concurrent Efficiency|
| 1| 0.46| 2188| 2188| 1| 100%|
@@ -74,7 +53,6 @@ tests the code base for concurrent efficiency.
Throughput is users added per second.
-
h3. JDBC
Same as above, using a local MySQL Instance.
@@ -93,9 +71,7 @@ Same as above, using a local MySQL Instance.
h3. Cassandra
-Using an untuned OOTB Cassandra instance running on the same box as the test, fighting for processor Cores.
-
-
+Using an untuned OOTB Cassandra instance running on the same box as the test, fighting for processor Cores.
|Threads|Time(s)|Throughput|Throughput per thread|Speedup|Concurrent Efficiency|
| 1| 1.14| 873| 873| 1| 100%|
@@ -112,7 +88,11 @@ Using an untuned OOTB Cassandra instance running on the same box as the test, fi
Throughput is users added per second.
+So far it looks like the code is concurrent, but MySQL is considerably slower than Cassandra or Memory. Below the Fighting for cores the box doesn't have enough CPUs to support the DB if present and the code.
+
+
+h2. Contributions, Patches, and License.
-So far it looks like the code is concurrent, but MySQL is considerably slower than Cassandra or Memory. Below the Fighting for cores
-the box doesn't have enough CPUs to support the DB if present and the code.
+The code in this code base is (c) Timefields Ltd and licensed to the Sakai Foundation under a Apache 2 Software License. Before making a contribution by means of a patch or otherwise please ensure that you read and understand the terms under which a patch or contribution will be accepted, outlined in the NOTICES file. All patches and contributions are made under those terms with no exceptions.
+I am sorry if this sounds a bit legal, but I want to be able to always license this software to the Sakai Foundation under an Apache 2 license and so I have to insist that no contributions are made that would prevent that from happening. I can't ask everyone who submits a patch to sign a legal document, so this is the next best thing. If you have a problem with this approach, please email me and we can try and work it out.
\ No newline at end of file
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 00000000..de2b11c6
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,278 @@
+
+content0 and
+ * content1. The inputs are not modified.
+ *
+ * @param content0
+ * @param content1
+ * @return A {@link Set} of {@link String} that are the keys of properties that are in
+ * content0 but not in content1.
+ */
+ public static Setkeys0 and
+ * keys1. The inputs are not modified.
+ *
+ * @param keys0
+ * @param key1
+ * @return A {@link Set} of {@link String} that are the keys that are in
+ * content0 but not in content1.
+ */
+ public static
+ * Please note, if you create a Content object using the public constructor, + * that object will have no children until it is saved and re-loaded by the + * ContentManager. Any attempt to list children of the newly created Content + * instance will result in an empty iterator. + *
+ ** If you need to make changes to a Content object, get it out of the store, * with contentManager.get(path); then change some properties before performing * a contentManager.update(contentObject); Transactions are managed by the * underlying store implementation and are not actively managed in the - * cotnentManager. If your underlying store is not transactional, the update + * contentManager. If your underlying store is not transactional, the update * operation will persist directly to the underlying store. Concurrent threads * in the same JVM may retrieve the same underlying data from the content store - * but each cotnentManager will operate on its own set of contentObjects - * isolated from other cotnentManagers until the update operation is completed. + * but each contentManager will operate on its own set of contentObjects + * isolated from other contentManagers until the update operation is completed. *
*/ public class Content extends InternalContent { /** * Create a brand new content object not connected to the underlying store. - * To save use contentManager.update(contentObject); + * To save use contentManager.update(contentObject); Since the object is not + * connected to the underlying store, it not have any children. Only Content + * objects loaded from the underlying store with ContentManager.get(path) + * are connected to the underlying store and have children. This is the case + * even if the path of the Content instance created via the public + * constructor exists within the underlying content store. * * @param path * the path in the store that should not already exist. If it * does exist, this new object will overwrite. * @param content * a map of initial content metadata. - * @param + * @param */ public Content(String path, java.util.Maptrue, append from as the latest content at
+ * to. If false, delete to before
+ * copying from.
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ */
+ List move(String from, String to, boolean force,
+ boolean keepDestinationHistory) throws AccessDeniedException,
+ StorageClientException;
+
+ /**
+ * Create a Link. Links place a pointer to real content located at the to path, in the
+ * from path. Modifications to the underlying content are reflected in both locations.
+ * Permissions are controlled by the location and not the underlying content.
+ *
+ * @param from
+ * the source of the link (the soft part), must not exist.
+ * @param to
+ * the destination, must exist
+ * @throws AccessDeniedException
+ * if the user cant read the to and write the from
+ * @throws StorageClientException
+ */
void link(String from, String to) throws AccessDeniedException, StorageClientException;
/**
@@ -290,4 +368,86 @@ InputStream getVersionInputStream(String path, String versionId) throws AccessDe
List getVersionHistory(String path) throws AccessDeniedException,
StorageClientException;
+ /**
+ * Gets a lazy iterator of child paths.
+ * @param path the parent path.
+ * @return
+ * @throws StorageClientException
+ */
+ Iterator listChildPaths(String path) throws StorageClientException;
+
+ /**
+ * Get a lazy iterator of child content objects.
+ * @param path
+ * @return
+ * @throws StorageClientException
+ */
+ Iterator listChildren(String path) throws StorageClientException;
+
+ /**
+ * @param path the path of the content node
+ * @param streamId the stream id, null for the default stream
+ * @return true if the stream id is present.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ boolean hasBody(String path, String streamId) throws StorageClientException, AccessDeniedException;
+
+ /**
+ * Sets the principal Token Resolver for all subsequent requests using this
+ * session. When the ContentManager is invoked it will consult the supplied
+ * principal Token Resolver to locate any extra tokens that have been
+ * granted.
+ *
+ * @param principalTokenResolver
+ */
+ void setPrincipalTokenResolver(PrincipalTokenResolver principalTokenResolver);
+
+ /**
+ * Clear the principal Token Resolver
+ */
+ void cleanPrincipalTokenResolver();
+
+
+ /**
+ * @param path cause an event to be emitted for the path that will cause a refresh.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void triggerRefresh(String path) throws StorageClientException, AccessDeniedException;
+
+
+ /**
+ * Cause an event to be emitted for all items.
+ * @throws StorageClientException
+ */
+ void triggerRefreshAll() throws StorageClientException;
+
+ /**
+ * Replace the content at content.getPath() with content. This
+ * sets any properties in content to new RemoveProperty() if
+ * there exists a property in the current version at content.getPath() that
+ * is missing from content.
+ *
+ * @param content
+ * The content to replace at content.getPath().
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void replace(Content content) throws AccessDeniedException, StorageClientException;
+
+ /**
+ * Replace the content at content.getPath() with content. This
+ * sets any properties in content to new RemoveProperty() if
+ * there exists a property in the current version at content.getPath() that
+ * is missing from content.
+ *
+ * @param content
+ * The content to replace at content.getPath().
+ * @param withTouch
+ * Whether to the update timestamp of the content.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void replace(Content content, boolean withTouch) throws AccessDeniedException, StorageClientException;
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java
new file mode 100644
index 00000000..a0bba217
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java
@@ -0,0 +1,14 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+public class AlreadyLockedException extends Exception {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -6198174336492911030L;
+
+ public AlreadyLockedException(String path) {
+ super("Lock path: "+path);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java
new file mode 100644
index 00000000..132d7e29
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java
@@ -0,0 +1,78 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+
+/**
+ * A simple hierarchical lock manager with tokens to identify locks.
+ * Implementations of the interface should not be bound to the content system.
+ * Locks live in their own hierarchy, and may exist even if there is not object
+ * present at that location in any other hierarchy.
+ *
+ * @author ieb
+ *
+ */
+public interface LockManager {
+
+ /**
+ * Locks a path returning a token for the lock if successful, null if not
+ *
+ * @param path
+ * the path to lock
+ * @param timeoutInSeconds
+ * ttl for the lock in s from the time it was created.
+ * @param extra
+ * any extra information to be stored with the lock.
+ * @return the lock token.
+ * @throws StorageClientException
+ * @throws AlreadyLockedException
+ */
+ String lock(String path, long timeoutInSeconds, String extra) throws StorageClientException,
+ AlreadyLockedException;
+
+ /**
+ * Unlock a path for a given token, if the token and current user match.
+ *
+ * @param path
+ * the path
+ * @param token
+ * the token.
+ * @throws StorageClientException
+ */
+ void unlock(String path, String token) throws StorageClientException;
+
+ /**
+ * Get the lock state for a path given a token
+ *
+ * @param path
+ * the path
+ * @param token
+ * the token
+ * @return a lock state object which indicates if the token is current and
+ * bound to the current user. Lock state also indicates the location
+ * of the current lock.
+ * @throws StorageClientException
+ */
+ LockState getLockState(String path, String token) throws StorageClientException;
+
+ /**
+ * Check the it path is locked.
+ *
+ * @param path
+ * the path.
+ * @return true if the path is locked.
+ * @throws StorageClientException
+ */
+ boolean isLocked(String path) throws StorageClientException;
+
+ /**
+ * Refresh the lock keeping the same token.
+ * @param path
+ * @param timeoutInSeconds
+ * @param string
+ * @param token
+ * @return the token, which should be the same.
+ * @throws StorageClientException
+ */
+ String refreshLock(String path, long timeoutInSeconds, String extra, String token) throws StorageClientException;
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java
new file mode 100644
index 00000000..0874c9f2
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java
@@ -0,0 +1,79 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+
+public class LockState {
+
+ private static final LockState NOT_LOCKED = new LockState(null, false, null, false, false,
+ null, null);
+ private final boolean isOwner;
+ private final String owner;
+ private final String path;
+ private final boolean locked;
+ private String token;
+ private String extra;
+ private boolean matchedToken;
+
+ public LockState(String path, boolean isOwner, String owner, boolean locked,
+ boolean matchedToken, String token, String extra) {
+ this.path = path;
+ this.isOwner = isOwner;
+ this.owner = owner;
+ this.locked = locked;
+ this.matchedToken = matchedToken;
+ this.token = token;
+ this.extra = extra;
+ }
+
+ public static LockState getOwnerLockedToken(String path, String owner, String token,
+ String extra) {
+ return new LockState(path, true, owner, true, true, token, extra);
+ }
+
+ public static LockState getOwnerLockedNoToken(String path, String owner, String token,
+ String extra) {
+ return new LockState(path, true, owner, true, false, token, extra);
+ }
+
+ public static LockState getUserLocked(String path, String owner, String token, String extra) {
+ return new LockState(path, false, owner, true, false, token, extra);
+ }
+
+ public static LockState getNotLocked() {
+ return NOT_LOCKED;
+ }
+
+ public boolean isOwner() {
+ return isOwner;
+ }
+
+ public String getLockPath() {
+ return path;
+ }
+
+ public boolean isLocked() {
+ return locked;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public boolean hasMatchedToken() {
+ return matchedToken;
+ }
+
+ public String getExtra() {
+ return extra;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ @Override
+ public String toString() {
+ return " isOwner:" + isOwner + " owner:" + owner + " locked:" + locked + " matchedToken:"
+ + matchedToken + " token:" + token + " extra:[" + extra + "]";
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java
new file mode 100644
index 00000000..90915c5f
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java
@@ -0,0 +1,71 @@
+package org.sakaiproject.nakamura.api.lite.util;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EnabledPeriod {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EnabledPeriod.class);
+
+ public static boolean isInEnabledPeriod(String enabledPeriod) {
+ Calendar[] period = getEnabledPeriod(enabledPeriod);
+ Calendar now = new ISO8601Date();
+ now.setTimeInMillis(System.currentTimeMillis());
+ if (period[0] != null && period[0].compareTo(now) > 0) {
+ return false;
+ }
+ if (period[1] != null && period[1].compareTo(now) <= 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public static Calendar[] getEnabledPeriod(String enabledPeriod) {
+ try {
+ if (enabledPeriod != null) {
+ enabledPeriod = enabledPeriod.trim();
+ if (enabledPeriod.startsWith(",")) {
+ return new Calendar[] { null, new ISO8601Date(enabledPeriod.substring(1)) };
+ } else if (enabledPeriod.endsWith(",")) {
+ return new Calendar[] {
+ new ISO8601Date(enabledPeriod.substring(0, enabledPeriod.length() - 1)),
+ null };
+ } else {
+ String[] period = StringUtils.split(enabledPeriod, ",");
+ return new Calendar[] { new ISO8601Date(period[0]), new ISO8601Date(period[1]) };
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ LOGGER.debug("Invalid date specified ", e);
+ }
+ return new Calendar[] { null, null };
+ }
+
+ public static String getEnableValue(long from, long to, boolean day, TimeZone zone) {
+ StringBuilder sb = new StringBuilder();
+ if (from > 0) {
+ ISO8601Date before = new ISO8601Date();
+ before.setTimeInMillis(from);
+ before.setTimeZone(zone);
+ before.setDate(day);
+ sb.append(before.toString());
+ }
+ sb.append(",");
+ if (to > 0) {
+ ISO8601Date after = new ISO8601Date();
+ after.setTimeInMillis(to);
+ after.setTimeZone(zone);
+ after.setDate(day);
+ sb.append(after.toString());
+ }
+ if (sb.length() > 1) {
+ return sb.toString();
+ }
+ return null;
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java
new file mode 100644
index 00000000..92fe6ea1
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java
@@ -0,0 +1,200 @@
+package org.sakaiproject.nakamura.api.lite.util;
+
+import java.util.Calendar;
+import java.util.Formatter;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ *
+ */
+public class ISO8601Date extends GregorianCalendar {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 5115079662422026445L;
+ private boolean date;
+
+ /*
+ * 2010-03-17 Separate date and time in UTC: 2010-03-17 06:33Z Combined date
+ * and time in UTC: 2010-03-17T06:33Z
+ */
+ /**
+ *
+ */
+ public ISO8601Date() {
+ date = false;
+ }
+
+ public ISO8601Date(String spec) {
+ int l = spec.length();
+ int year = -1;
+ int month = -1;
+ int day = -1;
+ int hour = -1;
+ int min = -1;
+ int sec = -1;
+ TimeZone z = null;
+ date = false;
+ switch (l) {
+ case 16:// 19970714T170000Z
+ case 18:// 19970714T170000+01
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = Integer.parseInt(spec.substring(9, 11));
+ min = Integer.parseInt(spec.substring(11, 13));
+ sec = Integer.parseInt(spec.substring(13, 15));
+ if ('Z' == spec.charAt(l - 1)) {
+ z = TimeZone.getTimeZone("GMT");
+ } else {
+ z = TimeZone.getTimeZone("GMT" + spec.substring(15));
+ }
+ break;
+ case 20: // 1997-07-14T17:00:00Z // 19970714T170000+0100
+ if ('Z' == spec.charAt(l - 1)) {
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = Integer.parseInt(spec.substring(11, 13));
+ min = Integer.parseInt(spec.substring(14, 16));
+ sec = Integer.parseInt(spec.substring(17, 19));
+ z = TimeZone.getTimeZone("UTC");
+ } else {
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = Integer.parseInt(spec.substring(9, 11));
+ min = Integer.parseInt(spec.substring(11, 13));
+ sec = Integer.parseInt(spec.substring(13, 15));
+ z = TimeZone.getTimeZone("GMT" + spec.substring(15));
+ }
+ break;
+ case 22: // 1997-07-14T17:00:00+01
+ case 25: // 1997-07-14T17:00:00+01:00
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = Integer.parseInt(spec.substring(11, 13));
+ min = Integer.parseInt(spec.substring(14, 16));
+ sec = Integer.parseInt(spec.substring(17, 19));
+ z = TimeZone.getTimeZone("GMT" + spec.substring(19));
+ date = false;
+ break;
+ case 8: // 19970714
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = 0;
+ min = 0;
+ sec = 0;
+ z = TimeZone.getDefault(); // we really need to know the timezone of
+ // the user for
+ // this.
+ date = true;
+ break;
+ case 10: // 1997-07-14
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = 0;
+ min = 0;
+ sec = 0;
+ z = TimeZone.getDefault(); // we really need to know the timezone of
+ // the user for
+ // this.
+ date = true;
+ break;
+ default:
+ throw new IllegalArgumentException("Illeagal ISO8601 Date Time " + spec);
+ }
+ if (z == null) {
+ throw new IllegalArgumentException(
+ "Time Zone incorrectly formatted, must be one of Z or +00:00 or +0000. Time was "
+ + spec);
+ }
+ setTimeZone(z);
+ set(MILLISECOND, 0);
+ set(year, month - 1, day, hour, min, sec);
+ }
+
+ @Override
+ public int compareTo(Calendar anotherCalendar) {
+ if ( date ) {
+ int cmp = get(YEAR) - anotherCalendar.get(YEAR);
+ if ( cmp == 0 ) {
+ cmp = get(DAY_OF_YEAR) - anotherCalendar.get(DAY_OF_YEAR);
+ }
+ return cmp;
+ }
+ return super.compareTo(anotherCalendar);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if ( obj instanceof ISO8601Date ) {
+ ISO8601Date d = (ISO8601Date) obj;
+ if ( date && d.date ) {
+ return get(YEAR) == d.get(YEAR) && get(DAY_OF_YEAR) == d.get(DAY_OF_YEAR);
+ } else if (date != d.date ) {
+ return false;
+ } else {
+ return super.equals(obj);
+ }
+ }
+ if ( date ) {
+ return false;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ Formatter formatter = new Formatter();
+ int year = get(YEAR);
+ int month = get(MONTH) + 1;
+ int day = get(DAY_OF_MONTH);
+ int hour = get(HOUR_OF_DAY);
+ int min = get(MINUTE);
+ int second = get(SECOND);
+ if (date) {
+ formatter.format("%04d-%02d-%02d", year, month, day);
+ } else {
+ // this prints out the offset, not the time zone name, but it takes
+ // into account DST if in effect for the time in question. Not that
+ // is
+ // not changed by the time of printing. This was checked on 23/11/2011.
+ long offset = getTimeZone().getOffset(getTimeInMillis()) / (60000L);
+ int hoffset = (int) (offset / 60L);
+ int minoffset = (int) (offset % 60L);
+ if (offset == 0) {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02dZ", year, month, day, hour, min,
+ second);
+ } else if (offset < 0) {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02d-%02d:%02d", year, month, day, hour,
+ min, second, -hoffset, -minoffset);
+ } else {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02d+%02d:%02d", year, month, day, hour,
+ min, second, hoffset, minoffset);
+ }
+ }
+ return formatter.toString();
+ }
+
+ /**
+ * @param b
+ */
+ public void setDate(boolean b) {
+ date = b;
+ }
+
+ public boolean isDate() {
+ return date;
+ }
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java
new file mode 100644
index 00000000..dd716676
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.util;
+
+import org.sakaiproject.nakamura.lite.storage.spi.CachableDisposableIterator;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposer;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * A Iterator wrapper that pre-emptively checks the next value in the underlying iterator before responding true to hasNext().
+ * @param
+ */
+public abstract class PreemptiveIterator implements Iterator, CachableDisposableIterator {
+
+ private static final int UNDETERMINED = 0;
+ private static final int TRUE = 1;
+ private static final int FALSE = -1;
+ private int lastCheck = UNDETERMINED;
+ private Disposer disposer;
+
+ protected abstract boolean internalHasNext();
+
+ protected abstract T internalNext();
+
+ /**
+ * By default a preemptive iterator does not cache. Override this method to make it cache.
+ */
+ @Override
+ public Map getResultsMap() {
+ return null;
+ }
+
+ public final boolean hasNext() {
+ if (lastCheck == FALSE) {
+ return false;
+ }
+ if (lastCheck != UNDETERMINED) {
+ return (lastCheck == TRUE);
+ }
+ if (internalHasNext()) {
+ lastCheck = TRUE;
+ return true;
+ }
+ lastCheck = FALSE;
+ return false;
+ }
+
+ public final T next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ lastCheck = UNDETERMINED;
+ return internalNext();
+ }
+
+ public final void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void close() {
+ if ( disposer != null ) {
+ disposer.unregisterDisposable(this);
+ }
+ }
+
+ public void setDisposer(Disposer disposer) {
+ this.disposer = disposer;
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
similarity index 94%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
index 07008c0f..0476dfe5 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
@@ -1,20 +1,19 @@
/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
+ * regarding copyright ownership. The SF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
+ * with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
*/
package org.sakaiproject.nakamura.api.lite.util;
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java b/core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
similarity index 61%
rename from src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
index 118a20c8..4e25a65a 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
@@ -1,23 +1,42 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
package org.sakaiproject.nakamura.lite;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
+import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableActivator;
-import org.sakaiproject.nakamura.lite.content.BlockContentHelper;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
-import org.sakaiproject.nakamura.lite.storage.StorageClientPool;
import org.sakaiproject.nakamura.lite.storage.mem.MemoryStorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.content.BlockContentHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.Map;
/**
- * Utiltiy class to create an entirely in memorty Sparse Repository, usefull for
+ * Utility class to create an entirely in memory Sparse Repository, useful for
* testing or bulk internal modifications.
*/
public class BaseMemoryRepository {
@@ -29,9 +48,7 @@ public class BaseMemoryRepository {
private RepositoryImpl repository;
public BaseMemoryRepository() throws StorageClientException, AccessDeniedException,
- ClientPoolException, ClassNotFoundException {
- clientPool = getClientPool();
- client = clientPool.getClient();
+ ClientPoolException, ClassNotFoundException, IOException {
configuration = new ConfigurationImpl();
Map properties = Maps.newHashMap();
properties.put("keyspace", "n");
@@ -39,6 +56,8 @@ public BaseMemoryRepository() throws StorageClientException, AccessDeniedExcepti
properties.put("authorizable-column-family", "au");
properties.put("content-column-family", "cn");
configuration.activate(properties);
+ clientPool = getClientPool(configuration);
+ client = clientPool.getClient();
AuthorizableActivator authorizableActivator = new AuthorizableActivator(client,
configuration);
authorizableActivator.setup();
@@ -56,10 +75,11 @@ public void close() {
client.close();
}
- protected StorageClientPool getClientPool() throws ClassNotFoundException {
+ protected StorageClientPool getClientPool(Configuration configuration) throws ClassNotFoundException {
MemoryStorageClientPool cp = new MemoryStorageClientPool();
cp.activate(ImmutableMap.of("test", (Object) "test",
- BlockContentHelper.CONFIG_MAX_CHUNKS_PER_BLOCK, 9));
+ BlockContentHelper.CONFIG_MAX_CHUNKS_PER_BLOCK, 9,
+ Configuration.class.getName(), configuration));
return cp;
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java
new file mode 100644
index 00000000..3476a00e
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.lite.storage.spi.DirectCacheAccess;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposable;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposer;
+import org.sakaiproject.nakamura.lite.storage.spi.RowHasher;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.Map;
+
+/**
+ * Extend this class to add caching to a Manager class.
+ */
+public abstract class CachingManagerImpl implements DirectCacheAccess {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CachingManagerImpl.class);
+ private Map sharedCache;
+ private StorageClient client;
+ private long managerId;
+ private static SecureRandom secureRandom = new SecureRandom(); // need to assume that the secure random will be reasonably quick to start up
+
+ /**
+ * Create a new {@link CachingManagerImpl}
+ * @param client a client to the underlying storage engine
+ * @param sharedCache the cache where the objects will be stored
+ */
+ public CachingManagerImpl(StorageClient client, Map sharedCache) {
+ this.client = client;
+ this.sharedCache = sharedCache;
+ managerId = getManagerId();
+ }
+
+ private long getManagerId() {
+ // needs to have a low probability of clashing with any other Cache manager in the cluster.
+ // no idea what the probability of a clash is here, although I assume its lowish.
+ return secureRandom.nextLong();
+ }
+
+ /**
+ * Try to retrieve an object from the cache.
+ * Has the side-effect of loading an uncached object into cache the first time.
+ * @param keySpace the key space we're operating in.
+ * @param columnFamily the column family for the object
+ * @param key the object key
+ * @return the object or null if not cached and not found
+ * @throws StorageClientException
+ */
+ protected Map getCached(String keySpace, String columnFamily, String key)
+ throws StorageClientException {
+ Map m = null;
+ String cacheKey = getCacheKey(keySpace, columnFamily, key);
+
+ CacheHolder cacheHolder = getFromCacheInternal(cacheKey);
+ if (cacheHolder != null ) {
+ m = cacheHolder.get();
+ if ( m != null ) {
+ LOGGER.debug("Cache Hit {} {} {} ", new Object[] { cacheKey, cacheHolder, m });
+ }
+ }
+ if (m == null) {
+ m = client.get(keySpace, columnFamily, key);
+ if (m != null) {
+ LOGGER.debug("Cache Miss, Found Map {} {}", cacheKey, m);
+ }
+ putToCacheInternal(cacheKey, new CacheHolder(m), true);
+ }
+ return m;
+ }
+ public void putToCache(String cacheKey, CacheHolder cacheHolder) {
+ putToCache(cacheKey, cacheHolder, false);
+ }
+
+ public void putToCache(String cacheKey, CacheHolder cacheHolder, boolean respectDeletes) {
+ if ( client instanceof RowHasher ) {
+ putToCacheInternal(cacheKey, cacheHolder, respectDeletes);
+ }
+ }
+
+ private void putToCacheInternal(String cacheKey, CacheHolder cacheHolder, boolean respectDeletes) {
+ if (sharedCache != null) {
+ if ( respectDeletes ) {
+ CacheHolder ch = sharedCache.get(cacheKey);
+ if ( ch != null && ch.get() == null ) {
+ // item is deleted, dont update it
+ return;
+ }
+ }
+ sharedCache.put(cacheKey, cacheHolder);
+ }
+ }
+ public CacheHolder getFromCache(String cacheKey) {
+ if ( client instanceof RowHasher ) {
+ return getFromCacheInternal(cacheKey);
+ }
+ return null;
+ }
+ private CacheHolder getFromCacheInternal(String cacheKey) {
+ if (sharedCache != null && sharedCache.containsKey(cacheKey)) {
+ return sharedCache.get(cacheKey);
+ }
+ return null;
+ }
+
+ protected abstract Logger getLogger();
+
+ /**
+ * Combine the parameters into a key suitable for storage and lookup in the cache.
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @return the cache key
+ * @throws StorageClientException
+ */
+ private String getCacheKey(String keySpace, String columnFamily, String key) throws StorageClientException {
+ if ( client instanceof RowHasher) {
+ return ((RowHasher) client).rowHash(keySpace, columnFamily, key);
+ }
+ return keySpace + ":" + columnFamily + ":" + key;
+ }
+
+ /**
+ * Remove this object from the cache. Note, StorageClient uses the word
+ * remove to mean delete. This method should do the same.
+ *
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @throws StorageClientException
+ */
+ protected void removeCached(String keySpace, String columnFamily, String key) throws StorageClientException {
+ if (sharedCache != null) {
+ // insert a replacement. This should cause an invalidation message to propagate in the cluster.
+ final String cacheKey = getCacheKey(keySpace, columnFamily, key);
+ putToCacheInternal(cacheKey, new CacheHolder(null, managerId), false);
+ LOGGER.debug("Marked as deleted in Cache {} ", cacheKey);
+ if ( client instanceof Disposer ) {
+ // we might want to change this to register the action as a commit handler rather than a disposable.
+ // it depends on if we think the delete is a transactional thing or a operational cache thing.
+ // at the moment, I am leaning towards an operational cache thing, since regardless of if
+ // the session commits or not, we want this to dispose when the session is closed, or commits.
+ ((Disposer)client).registerDisposable(new Disposable() {
+
+ @Override
+ public void setDisposer(Disposer disposer) {
+ }
+
+ @Override
+ public void close() {
+ CacheHolder ch = sharedCache.get(cacheKey);
+ if ( ch != null && ch.wasLockedTo(managerId)) {
+ sharedCache.remove(cacheKey);
+ LOGGER.debug("Removed deleted marker from Cache {} ", cacheKey);
+ }
+ }
+ });
+ }
+ }
+ client.remove(keySpace, columnFamily, key);
+
+ }
+
+ /**
+ * Put an object in the cache
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @param encodedProperties the object to be stored
+ * @param probablyNew whether or not this object is new.
+ * @throws StorageClientException
+ */
+ protected void putCached(String keySpace, String columnFamily, String key,
+ Map encodedProperties, boolean probablyNew)
+ throws StorageClientException {
+ String cacheKey = null;
+ if ( sharedCache != null ) {
+ cacheKey = getCacheKey(keySpace, columnFamily, key);
+ }
+ if ( sharedCache != null && !probablyNew ) {
+ CacheHolder ch = getFromCacheInternal(cacheKey);
+ if ( ch != null && ch.isLocked(this.managerId) ) {
+ LOGGER.debug("Is Locked {} ",ch);
+ return; // catch the case where another method creates while something is in the cache.
+ // this is a big assumption since if the item is not in the cache it will get updated
+ // there is no difference in sparsemap between create and update, they are all insert operations
+ // what we are really saying here is that inorder to update the item you have to have just got it
+ // and if you failed to get it, your update must have been a create operation. As long as the dwell time
+ // in the cache is longer than the lifetime of an active session then this will be true.
+ // if the lifetime of an active session is longer (like with a long running background operation)
+ // then you should expect to see race conditions at this point since the marker in the cache will have
+ // gone, and the marker in the database has gone, so the put operation, must be a create operation.
+ // To change this behavior we would need to differentiate more strongly between new and update and change
+ // probablyNew into certainlyNew, but that would probably break the BASIC assumption of the whole system.
+ // Update 2011-12-06 related to issue 136
+ // I am not certain this code is correct. What happens if the session wants to remove and then add items.
+ // the session will never get past this point, since sitting in the cache is a null CacheHolder preventing the session
+ // removing then adding.
+ // also, how long should the null cache holder be placed in there for ?
+ // I think the solution is to bind the null Cache holder to the instance of the caching manager that created it,
+ // let the null Cache holder last for 10s, and during that time only the CachingManagerImpl that created it can remove it.
+ }
+ }
+ LOGGER.debug("Saving {} {} {} {} ", new Object[] { keySpace, columnFamily, key,
+ encodedProperties });
+ client.insert(keySpace, columnFamily, key, encodedProperties, probablyNew);
+ if ( sharedCache != null ) {
+ // if we just added a value in, remove the key so that any stale state (including a previously deleted object is removed)
+ sharedCache.remove(cacheKey);
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java
new file mode 100644
index 00000000..59ba4b7b
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+@Component(immediate = true, metatype = true)
+@Service(value = Configuration.class)
+public class ConfigurationImpl implements Configuration {
+
+ @Property(value = "ac")
+ protected static final String ACL_COLUMN_FAMILY = "acl-column-family";
+ @Property(value = "n")
+ protected static final String KEYSPACE = "keyspace";
+ @Property(value = "au")
+ protected static final String AUTHORIZABLE_COLUMN_FAMILY = "authorizable-column-family";
+ @Property(value = "cn")
+ protected static final String CONTENT_COLUMN_FAMILY = "content-column-family";
+ @Property(value = "lk")
+ protected static final String LOCK_COLUMN_FAMILY = "lock-column-family";
+
+ protected static final String DEFAULT_INDEX_COLUMN_NAMES = "au:rep:principalName,au:type,cn:sling:resourceType," +
+ "cn:sakai:pooled-content-manager,cn:sakai:messagestore,cn:sakai:type,cn:sakai:marker,cn:sakai:tag-uuid," +
+ "cn:sakai:contactstorepath,cn:sakai:state,cn:_created,cn:sakai:category,cn:sakai:messagebox,cn:sakai:from," +
+ "cn:sakai:subject";
+
+ @Property(value=DEFAULT_INDEX_COLUMN_NAMES)
+ protected static final String INDEX_COLUMN_NAMES = "index-column-names";
+
+ private static final String DEFAULT_INDEX_COLUMN_TYPES = "cn:sakai:pooled-content-manager=String[],cn:sakai:category=String[]";
+
+ @Property(value=DEFAULT_INDEX_COLUMN_TYPES)
+ protected static final String INDEX_COLUMN_TYPES = "index-column-types";
+
+
+ private static final String SHAREDCONFIGPATH = "org/sakaiproject/nakamura/lite/shared.properties";
+
+ protected static final String SHAREDCONFIGPROPERTY = "sparseconfig";
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationImpl.class);
+
+
+ private String aclColumnFamily;
+ private String keySpace;
+ private String authorizableColumnFamily;
+ private String contentColumnFamily;
+ private String lockColumnFamily;
+ private String[] indexColumnNames;
+ private Map sharedProperties;
+ private String[] indexColumnTypes;
+
+ @SuppressWarnings("unchecked")
+ @Activate
+ public void activate(Map properties) throws IOException {
+ aclColumnFamily = StorageClientUtils.getSetting(properties.get(ACL_COLUMN_FAMILY), "ac");
+ keySpace = StorageClientUtils.getSetting(properties.get(KEYSPACE), "n");
+ authorizableColumnFamily = StorageClientUtils.getSetting(properties.get(AUTHORIZABLE_COLUMN_FAMILY), "au");
+ contentColumnFamily = StorageClientUtils.getSetting(properties.get(CONTENT_COLUMN_FAMILY), "cn");
+ lockColumnFamily = StorageClientUtils.getSetting(properties.get(LOCK_COLUMN_FAMILY), "ln");
+
+ // load defaults
+ // check the classpath
+ sharedProperties = Maps.newHashMap();
+ InputStream in = this.getClass().getClassLoader().getResourceAsStream(SHAREDCONFIGPATH);
+ if ( in != null ) {
+ Properties p = new Properties();
+ p.load(in);
+ in.close();
+ sharedProperties.putAll(Maps.fromProperties(p));
+ }
+ // Load from a properties file defiend on the command line
+ String osSharedConfigPath = System.getProperty(SHAREDCONFIGPROPERTY);
+ if ( osSharedConfigPath != null && StringUtils.isNotEmpty(osSharedConfigPath)) {
+ File f = new File(osSharedConfigPath);
+ if ( f.exists() && f.canRead() ) {
+ FileReader fr = new FileReader(f);
+ Properties p = new Properties();
+ p.load(fr);
+ fr.close();
+ sharedProperties.putAll(Maps.fromProperties(p));
+ } else {
+ LOGGER.warn("Unable to read shared config file {} specified by the system property {} ",f.getAbsolutePath(), SHAREDCONFIGPROPERTY);
+ }
+ }
+
+ // make the shared properties immutable.
+ sharedProperties = ImmutableMap.copyOf(sharedProperties);
+ indexColumnNames = StringUtils.split(getProperty(INDEX_COLUMN_NAMES,DEFAULT_INDEX_COLUMN_NAMES, sharedProperties, properties),',');
+ LOGGER.info("Using Configuration for Index Column Names as {}", Arrays.toString(indexColumnNames));
+ indexColumnTypes = StringUtils.split(getProperty(INDEX_COLUMN_TYPES,DEFAULT_INDEX_COLUMN_TYPES, sharedProperties, properties),',');
+
+
+
+
+ }
+
+ private String getProperty(String name, String defaultValue,
+ Map ...properties ) {
+ // if present in the shared properties, load the default from there.
+ String value = defaultValue;
+ for ( Map p : properties ) {
+ if ( p.containsKey(name) ) {
+ Object v = p.get(name);
+ if ( v != null && !defaultValue.equals(v)) {
+ value = String.valueOf(v);
+ LOGGER.debug("{} is configured as {}", value);
+ }
+ }
+ }
+ return value;
+
+ }
+
+ public String getAclColumnFamily() {
+ return aclColumnFamily;
+ }
+
+ public String getKeySpace() {
+ return keySpace;
+ }
+
+ public String getAuthorizableColumnFamily() {
+ return authorizableColumnFamily;
+ }
+
+ public String getContentColumnFamily() {
+ return contentColumnFamily;
+ }
+
+ public String getLockColumnFamily() {
+ return lockColumnFamily;
+ }
+
+ public String[] getIndexColumnNames() {
+ return indexColumnNames;
+ }
+ public Map getSharedConfig() {
+ return sharedProperties;
+ }
+
+ public String[] getIndexColumnTypes() {
+ return indexColumnTypes;
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java b/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java
new file mode 100644
index 00000000..8f255eb3
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import org.sakaiproject.nakamura.api.lite.StoreListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Map;
+
+public class LoggingStorageListener implements StoreListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LoggingStorageListener.class);
+ private boolean quiet;
+
+ public LoggingStorageListener(boolean quiet) {
+ this.quiet = quiet;
+ }
+
+ public LoggingStorageListener() {
+ this.quiet = false;
+ }
+
+ public void onDelete(String zone, String path, String user, String resourceType, Map beforeEvent,
+ String... attributes) {
+ if (!quiet) {
+ LOGGER.info("Delete {} {} {} {} {} ",
+ new Object[] { zone, path, user, resourceType, Arrays.toString(attributes) });
+ }
+ }
+
+ public void onUpdate(String zone, String path, String user, String resourceType, boolean isNew,
+ Map beforeEvent, String... attributes) {
+ if (!quiet) {
+ LOGGER.info("Update {} {} {} {} new:{} {} ", new Object[] { zone, path, user, resourceType, isNew,
+ Arrays.toString(attributes) });
+ }
+ }
+
+ public void onLogin(String userId, String sessionId) {
+ if (!quiet) {
+ LOGGER.info("Login {} {}", new Object[] { userId, sessionId });
+ }
+ }
+
+ public void onLogout(String userId, String sessionId) {
+ if (!quiet) {
+ LOGGER.info("Logout {} {}", new Object[] { userId, sessionId });
+ }
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
similarity index 78%
rename from src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
index 8b7dbf22..fc615920 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
@@ -1,4 +1,4 @@
-/*
+/**
* Licensed to the Sakai Foundation (SF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -15,16 +15,15 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-package org.sakaiproject.nakamura.lite.storage;
-
-import java.util.Iterator;
+package org.sakaiproject.nakamura.lite;
/**
- * Disposable Iterators must be closed when they have been used.
+ * A marker service for components that are disabled and perform an action on
+ * activation. (OSGi component validation requirement)
*
* @author ieb
*
- * @param
*/
-public interface DisposableIterator extends Iterator, Disposable {
+public interface ManualOperationService {
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java b/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java
new file mode 100644
index 00000000..783a8c4d
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java
@@ -0,0 +1,40 @@
+package org.sakaiproject.nakamura.lite;
+
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.StorageCacheManager;
+
+/**
+ * Unmanaged Caches are used where there is nothing else provided by the client.
+ * @author ieb
+ *
+ */
+public class NullCacheManagerX implements StorageCacheManager {
+
+
+
+ @Override
+ public Map getAccessControlCache() {
+ return null;
+ }
+
+ @Override
+ public Map getAuthorizableCache() {
+ return null;
+ }
+
+ @Override
+ public Map getContentCache() {
+ return null;
+ }
+
+ @Override
+ public Map getCache(String cacheName) {
+ return null;
+ }
+
+
+
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java b/core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
similarity index 73%
rename from src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
index 0636c41f..29807da6 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
@@ -1,3 +1,20 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
package org.sakaiproject.nakamura.lite;
import com.google.common.collect.ImmutableMap;
@@ -18,6 +35,10 @@
import java.util.Hashtable;
import java.util.Map;
+/**
+ * When this {@link StoreListener} is notified of a storage action
+ * it posts an OSGi {@link Event} to the {@link EventAdmin}
+ */
@Component(immediate = true, metatype = true)
@Service
public class OSGiStoreListener implements StoreListener {
@@ -60,15 +81,21 @@ public class OSGiStoreListener implements StoreListener {
}
- public void onDelete(String zone, String path, String user, String... attributes) {
+ /**
+ * {@inheritDoc}
+ */
+ public void onDelete(String zone, String path, String user, String resourceType, Map beforeEvent, String ... attributes) {
String topic = DEFAULT_DELETE_TOPIC;
if (deleteTopics.containsKey(zone)) {
topic = deleteTopics.get(zone);
}
- postEvent(topic, path, user, attributes);
+ postEvent(topic, path, user, resourceType, beforeEvent, attributes);
}
- public void onUpdate(String zone, String path, String user, boolean isNew, String... attributes) {
+ /**
+ * {@inheritDoc}
+ */
+ public void onUpdate(String zone, String path, String user, String resourceType, boolean isNew, Map beforeEvent, String... attributes) {
String topic = DEFAULT_UPDATE_TOPIC;
if (isNew) {
@@ -82,19 +109,29 @@ public void onUpdate(String zone, String path, String user, boolean isNew, Strin
}
}
- postEvent(topic, path, user, attributes);
+ postEvent(topic, path, user, resourceType, beforeEvent, attributes);
}
+ /**
+ * {@inheritDoc}
+ *
+ * No event is posted for these actions.
+ */
public void onLogin(String userid, String sessionID) {
LOGGER.debug("Login {} {} ", userid, sessionID);
}
+ /**
+ * {@inheritDoc}
+ *
+ * No event is posted for these actions.
+ */
public void onLogout(String userid, String sessionID) {
LOGGER.debug("Logout {} {} ", userid, sessionID);
}
- private void postEvent(String topic, String path, String user, String[] attributes) {
- final Dictionary properties = new Hashtable();
+ private void postEvent(String topic, String path, String user, String resourceType, Map beforeEvent, String[] attributes) {
+ final Dictionary properties = new Hashtable();
if (attributes != null) {
for (String attribute : attributes) {
String[] parts = StringUtils.split(attribute, ":", 2);
@@ -110,7 +147,13 @@ private void postEvent(String topic, String path, String user, String[] attribut
if (path != null) {
properties.put(PATH_PROPERTY, path);
}
+ if ( resourceType != null ) {
+ properties.put(RESOURCE_TYPE_PROPERTY, resourceType);
+ }
properties.put(USERID_PROPERTY, user);
+ if ( beforeEvent != null) {
+ properties.put(BEFORE_EVENT_PROPERTY, beforeEvent);
+ }
eventAdmin.postEvent(new Event(topic, properties));
}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
similarity index 65%
rename from src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
index beef8a03..d62bc88b 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
@@ -22,18 +22,23 @@
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
+import org.sakaiproject.nakamura.api.lite.StorageCacheManager;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StoreListener;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableActivator;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
-import org.sakaiproject.nakamura.lite.storage.StorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClientPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Map;
@@ -41,19 +46,30 @@
@Service(value = Repository.class)
public class RepositoryImpl implements Repository {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryImpl.class);
+
@Reference
protected Configuration configuration;
@Reference
protected StorageClientPool clientPool;
-
- @Reference
+
+ @Reference
protected StoreListener storeListener;
+ @Reference
+ protected PrincipalValidatorResolver principalValidatorResolver;
public RepositoryImpl() {
}
+ public RepositoryImpl(Configuration configuration, StorageClientPool clientPool,
+ LoggingStorageListener listener) {
+ this.configuration = configuration;
+ this.clientPool = clientPool;
+ this.storeListener = listener;
+ }
+
@Activate
public void activate(Map properties) throws ClientPoolException,
StorageClientException, AccessDeniedException {
@@ -64,8 +80,11 @@ public void activate(Map properties) throws ClientPoolException,
configuration);
authorizableActivator.setup();
} finally {
- client.close();
- clientPool.getClient();
+ if (client != null) {
+ client.close();
+ } else {
+ LOGGER.error("Failed to actvate repository, probably failed to create default users");
+ }
}
}
@@ -93,17 +112,23 @@ public Session loginAdministrative(String username) throws StorageClientExceptio
return openSession(username);
}
+ public Session loginAdministrativeBypassEnable(String username) throws StorageClientException,
+ ClientPoolException, AccessDeniedException {
+ return openSessionBypassEnable(username);
+ }
+
private Session openSession(String username, String password) throws StorageClientException,
AccessDeniedException {
StorageClient client = null;
try {
client = clientPool.getClient();
- AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration);
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
User currentUser = authenticatorImpl.authenticate(username, password);
if (currentUser == null) {
throw new StorageClientException("User " + username + " cant login with password");
}
- return new SessionImpl(this, currentUser, client, configuration, clientPool.getStorageCacheManager(), storeListener);
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
} catch (ClientPoolException e) {
clientPool.getClient();
throw e;
@@ -119,18 +144,54 @@ private Session openSession(String username, String password) throws StorageClie
}
}
+ private Map getAuthorizableCache(StorageCacheManager storageCacheManager) {
+ if ( storageCacheManager != null ) {
+ return storageCacheManager.getAuthorizableCache();
+ }
+ return null;
+ }
+
private Session openSession(String username) throws StorageClientException,
AccessDeniedException {
StorageClient client = null;
try {
client = clientPool.getClient();
- AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration);
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
User currentUser = authenticatorImpl.systemAuthenticate(username);
if (currentUser == null) {
throw new StorageClientException("User " + username
+ " does not exist, cant login administratively as this user");
}
- return new SessionImpl(this, currentUser, client, configuration, clientPool.getStorageCacheManager(), storeListener);
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
+ } catch (ClientPoolException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (StorageClientException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (AccessDeniedException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (Throwable e) {
+ clientPool.getClient();
+ throw new StorageClientException(e.getMessage(), e);
+ }
+ }
+
+ private Session openSessionBypassEnable(String username) throws StorageClientException,
+ AccessDeniedException {
+ StorageClient client = null;
+ try {
+ client = clientPool.getClient();
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
+ User currentUser = authenticatorImpl.systemAuthenticateBypassEnable(username);
+ if (currentUser == null) {
+ throw new StorageClientException("User " + username
+ + " does not exist, cant login administratively as this user");
+ }
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
} catch (ClientPoolException e) {
clientPool.getClient();
throw e;
@@ -156,7 +217,7 @@ public void setConnectionPool(StorageClientPool connectionPool) {
public void setStorageListener(StoreListener storeListener) {
this.storeListener = storeListener;
-
+
}
}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
similarity index 57%
rename from src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
index 127b1ca6..6edd2f16 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
@@ -17,7 +17,11 @@
*/
package org.sakaiproject.nakamura.lite;
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
+import org.sakaiproject.nakamura.api.lite.CommitHandler;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
@@ -26,48 +30,80 @@
import org.sakaiproject.nakamura.api.lite.StoreListener;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Authenticator;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableManagerImpl;
import org.sakaiproject.nakamura.lite.content.ContentManagerImpl;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.lock.LockManagerImpl;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Maps;
public class SessionImpl implements Session {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionImpl.class);
private AccessControlManagerImpl accessControlManager;
private ContentManagerImpl contentManager;
private AuthorizableManagerImpl authorizableManager;
+ private LockManagerImpl lockManager;
private User currentUser;
private Repository repository;
private Exception closedAt;
private StorageClient client;
private Authenticator authenticator;
private StoreListener storeListener;
+ private Map commitHandlers = Maps.newLinkedHashMap();
+ private StorageCacheManager storageCacheManager;
+ private Configuration configuration;
+ private static long nagclient;
public SessionImpl(Repository repository, User currentUser, StorageClient client,
- Configuration configuration, StorageCacheManager storageCacheManager, StoreListener storeListener)
+ Configuration configuration, StorageCacheManager storageCacheManager,
+ StoreListener storeListener, PrincipalValidatorResolver principalValidatorResolver)
throws ClientPoolException, StorageClientException, AccessDeniedException {
this.currentUser = currentUser;
this.repository = repository;
this.client = client;
+ this.storageCacheManager = storageCacheManager;
+ this.storeListener = storeListener;
+ this.configuration = configuration;
+
+ if ( this.storageCacheManager == null ) {
+ if ( (nagclient % 1000) == 0 ) {
+ LOGGER.warn("No Cache Manager, All Caching disabled, please provide an Implementation of NamedCacheManager. This message will appear every 1000th time a session is created. ");
+ }
+ nagclient++;
+ }
accessControlManager = new AccessControlManagerImpl(client, currentUser, configuration,
- storageCacheManager.getAccessControlCache(), storeListener);
- authorizableManager = new AuthorizableManagerImpl(currentUser, client, configuration,
- accessControlManager, storageCacheManager.getAuthorizableCache(), storeListener);
+ getCache(configuration.getAclColumnFamily()), storeListener,
+ principalValidatorResolver);
+ Map authorizableCache = getCache(configuration
+ .getAuthorizableColumnFamily());
+ authorizableManager = new AuthorizableManagerImpl(currentUser, this, client, configuration,
+ accessControlManager, authorizableCache, storeListener);
- contentManager = new ContentManagerImpl(client, accessControlManager, configuration, storageCacheManager.getContentCache(), storeListener);
+ contentManager = new ContentManagerImpl(client, accessControlManager, configuration,
+ getCache(configuration.getContentColumnFamily()), storeListener);
+
+ lockManager = new LockManagerImpl(client, configuration, currentUser,
+ getCache(configuration.getLockColumnFamily()));
+
+ authenticator = new AuthenticatorImpl(client, configuration, authorizableCache);
- authenticator = new AuthenticatorImpl(client, configuration);
- this.storeListener = storeListener;
storeListener.onLogin(currentUser.getId(), this.toString());
}
public void logout() throws ClientPoolException {
if (closedAt == null) {
+ commit();
accessControlManager.close();
authorizableManager.close();
contentManager.close();
+ lockManager.close();
client.close();
accessControlManager = null;
authorizableManager = null;
@@ -94,6 +130,11 @@ public ContentManagerImpl getContentManager() throws StorageClientException {
return contentManager;
}
+ public LockManagerImpl getLockManager() throws StorageClientException {
+ check();
+ return lockManager;
+ }
+
public Authenticator getAuthenticator() throws StorageClientException {
check();
return authenticator;
@@ -114,4 +155,39 @@ private void check() throws StorageClientException {
}
}
+ public StorageClient getClient() {
+ return client;
+ }
+
+ public void addCommitHandler(String key, CommitHandler commitHandler) {
+ synchronized (commitHandlers) {
+ commitHandlers.put(key, commitHandler);
+ }
+ }
+
+ public void commit() {
+ synchronized (commitHandlers) {
+ for (CommitHandler commitHandler : commitHandlers.values()) {
+ commitHandler.commit();
+ }
+ commitHandlers.clear();
+ }
+ }
+
+ public Map getCache(String columnFamily) {
+ if (storageCacheManager != null) {
+ if (configuration.getAuthorizableColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAuthorizableCache();
+ }
+ if (configuration.getAclColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAccessControlCache();
+ }
+ if (configuration.getContentColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getContentCache();
+ }
+ return storageCacheManager.getCache(columnFamily);
+ }
+ return null;
+ }
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java
new file mode 100644
index 00000000..d4ebedd4
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java
@@ -0,0 +1,731 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.sakaiproject.nakamura.api.lite.StoreListener;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessControlManager;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AclModification;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permission;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalTokenResolver;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Security;
+import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
+import org.sakaiproject.nakamura.api.lite.authorizable.AuthorizableManager;
+import org.sakaiproject.nakamura.api.lite.authorizable.Group;
+import org.sakaiproject.nakamura.api.lite.authorizable.User;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import edu.umd.cs.findbugs.annotations.SuppressWarnings;
+
+public class AccessControlManagerImpl extends CachingManagerImpl implements AccessControlManager {
+
+ private static final String _SECRET_KEY = "_secretKey";
+ private static final String _PATH = "_aclPath";
+ private static final String _OBJECT_TYPE = "_aclType";
+ public static final String _KEY = "_aclKey";
+ private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlManagerImpl.class);
+ private static final Set PROTECTED_PROPERTIES = ImmutableSet.of(_SECRET_KEY);
+ private static final Set READ_ONLY_PROPERTIES = ImmutableSet.of(_SECRET_KEY, _PATH, _OBJECT_TYPE, _KEY);
+ private User user;
+ private String keySpace;
+ private String aclColumnFamily;
+ private Map cache = new ConcurrentHashMap();
+ private boolean closed;
+ private StoreListener storeListener;
+ private PrincipalTokenValidator principalTokenValidator;
+ private PrincipalTokenResolver principalTokenResolver;
+ private SecureRandom secureRandom;
+ private AuthorizableManager authorizableManager;
+ private Map principalCache = new ConcurrentHashMap();
+ private ThreadLocal principalRecursionLock = new ThreadLocal();
+ private ThreadBoundStackReferenceCounter compilingPermissions = new ThreadBoundStackReferenceCounter();
+
+ public AccessControlManagerImpl(StorageClient client, User currentUser, Configuration config,
+ Map sharedCache, StoreListener storeListener, PrincipalValidatorResolver principalValidatorResolver) throws StorageClientException {
+ super(client, sharedCache);
+ this.user = currentUser;
+ this.aclColumnFamily = config.getAclColumnFamily();
+ this.keySpace = config.getKeySpace();
+ closed = false;
+ this.storeListener = storeListener;
+ principalTokenValidator = new PrincipalTokenValidator(principalValidatorResolver);
+ secureRandom = new SecureRandom();
+ }
+
+ public Map getAcl(String objectType, String objectPath)
+ throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(objectType, objectPath, Permissions.CAN_READ_ACL);
+
+ String key = this.getAclKey(objectType, objectPath);
+ return StorageClientUtils.getFilterMap(getCached(keySpace, aclColumnFamily, key), null, null, PROTECTED_PROPERTIES, false);
+ }
+
+ /**
+ * Property principals are stored with keys of the form
+ * _pp_@@ where principal is a principal. For the
+ * acl to be selected for the used of this session, they must have that
+ * principal. property is the name of the property. g or d is grant or deny.
+ * The value of the ACE is the bitmap for the ACE. All ACEs are selected and
+ * processed to form the ACE for the user, returned as a PropertyAcl.
+ */
+ public PropertyAcl getPropertyAcl(String objectType, String objectPath) throws AccessDeniedException, StorageClientException {
+ checkOpen();
+ compilingPermissions.inc();
+ try {
+ String key = this.getAclKey(objectType, objectPath);
+ Map objectAcl = getCached(keySpace, aclColumnFamily, key);
+ Set orderedPrincipals = Sets.newLinkedHashSet();
+ {
+ String principal = user.getId();
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ orderedPrincipals.add(principal);
+ }
+ for (String principal : getPrincipals(user) ) {
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ orderedPrincipals.add(principal);
+ }
+ // Everyone must be the last principal to be applied
+ if (!User.ANON_USER.equals(user.getId())) {
+ orderedPrincipals.add(Group.EVERYONE);
+ }
+ // go through each principal
+ Map grants = Maps.newHashMap();
+ Map denies = Maps.newHashMap();
+ for ( String principal : orderedPrincipals) {
+ // got through each property
+ String ppk = PROPERTY_PRINCIPAL_STEM+principal;
+ for(Entry e : objectAcl.entrySet()) {
+ String k = e.getKey();
+ if ( k.startsWith(ppk)) {
+ String[] parts = StringUtils.split(k.substring(PROPERTY_PRINCIPAL_STEM.length()),"@");
+ String propertyName = parts[1];
+ if ( AclModification.isDeny(k)) {
+ int td = toInt(e.getValue());
+ denies.put(propertyName, toInt(denies.get(propertyName)) | td);
+ } else if ( AclModification.isGrant(k)) {
+ int tg = toInt(e.getValue());
+ grants.put(propertyName, toInt(grants.get(propertyName)) | tg);
+ }
+ }
+ }
+ }
+ // if the property has been granted, then that should remove the deny
+ for ( Entry g : grants.entrySet()) {
+ String k = g.getKey();
+ if ( denies.containsKey(k)) {
+ denies.put(k, toInt(denies.get(k)) & ~g.getValue());
+ }
+ }
+ return new PropertyAcl(denies);
+ } finally {
+ compilingPermissions.dec();
+ }
+
+ }
+
+
+ public Map getEffectiveAcl(String objectType, String objectPath)
+ throws StorageClientException, AccessDeniedException {
+ throw new UnsupportedOperationException("Nag someone to implement this");
+ }
+
+ // to sign a token we need setAcl permissions on the delegate path
+ /**
+ * Content Tokens activate ACEs for a user that holds the content token. The
+ * token is signed by the secret key associated with the target Object/acl
+ * and the token is token content item is then returned for the caller to
+ * save.
+ */
+ public void signContentToken(Content token, String securityZone, String objectPath) throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_WRITE_ACL);
+ check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_READ_ACL);
+ String key = this.getAclKey(securityZone, objectPath);
+ Map currentAcl = getCached(keySpace, aclColumnFamily, key);
+ String secretKey = (String) currentAcl.get(_SECRET_KEY);
+ principalTokenValidator.signToken(token, secretKey);
+ // the caller must save the target.
+ }
+
+ @SuppressWarnings(value="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", justification="Not correct, the line in question doesnt check for a null, so the check is not redundant")
+ public void setAcl(String objectType, String objectPath, AclModification[] aclModifications)
+ throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(objectType, objectPath, Permissions.CAN_WRITE_ACL);
+ check(objectType, objectPath, Permissions.CAN_READ_ACL);
+ String key = this.getAclKey(objectType, objectPath);
+ Map currentAcl = getCached(keySpace, aclColumnFamily, key);
+ if ( currentAcl == null ) {
+ currentAcl = Maps.newHashMap();
+ }
+ // every ACL gets a secret key, which avoids doing it later with a special call
+ Map modifications = Maps.newLinkedHashMap();
+ if ( !currentAcl.containsKey(_SECRET_KEY)) {
+ byte[] secretKeySeed = new byte[20];
+ secureRandom.nextBytes(secretKeySeed);
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA1");
+ modifications.put(_SECRET_KEY, Base64.encodeBase64URLSafeString(md.digest(secretKeySeed)));
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.error(e.getMessage(),e);
+ }
+ }
+ if ( !currentAcl.containsKey(_KEY)) {
+ modifications.put(_KEY, key);
+ modifications.put(_OBJECT_TYPE, objectType); // this is here to make data migration possible in the future
+ modifications.put(_PATH, objectPath); // same
+ }
+ for (AclModification m : aclModifications) {
+ String name = m.getAceKey();
+ if ( READ_ONLY_PROPERTIES.contains(name)) {
+ continue;
+ }
+ if (m.isRemove()) {
+ modifications.put(name, null);
+ } else {
+
+ int originalbitmap = getBitMap(name, modifications, currentAcl);
+ int modifiedbitmap = m.modify(originalbitmap);
+ LOGGER.debug("Adding Modification {} {} ",name, modifiedbitmap);
+ modifications.put(name, modifiedbitmap);
+
+ // KERN-1515
+ // We need to modify the opposite key to apply the
+ // reverse of the change we just made. Otherwise,
+ // you can end up with ACLs with contradictions, like:
+ // anonymous@g=1, anonymous@d=1
+ if (containsKey(inverseKeyOf(name), modifications, currentAcl)) {
+ // XOR gives us a mask of only the bits that changed
+ int difference = originalbitmap ^ modifiedbitmap;
+ int otherbitmap = toInt(getBitMap(inverseKeyOf(name), modifications, currentAcl));
+
+ // Zero out the bits that have been modified
+ //
+ // KERN-1887: This was originally toggling the modified bits
+ // using: "otherbitmap ^ difference", but this would
+ // incorrectly grant permissions in some cases (see JIRA
+ // issue). To avoid inconsistencies between grant and deny
+ // lists, setting a bit in one list should unset the
+ // corresponding bit in the other.
+ int modifiedotherbitmap = otherbitmap & ~difference;
+
+ if (otherbitmap != modifiedotherbitmap) {
+ // We made a change. Record our modification.
+ modifications.put(inverseKeyOf(name), modifiedotherbitmap);
+ }
+ }
+ }
+ }
+ LOGGER.debug("Updating ACL {} {} ", key, modifications);
+ putCached(keySpace, aclColumnFamily, key, modifications, (currentAcl == null || currentAcl.size() == 0));
+ storeListener.onUpdate(objectType, objectPath, getCurrentUserId(), "type:acl", false, null, "op:acl");
+ // clear the compiled cache for this session.
+ List keys = Lists.newArrayList();
+ for ( Entry e : cache.entrySet()) {
+ if (e.getKey().startsWith(key)) {
+ keys.add(e.getKey());
+ }
+ }
+ for ( String k : keys ) {
+ cache.remove(k);
+ }
+ }
+
+ private boolean containsKey(String name, Map map1,
+ Map map2) {
+ return map1.containsKey(name) || map2.containsKey(name);
+ }
+
+ private int getBitMap(String name, Map modifications,
+ Map currentAcl) {
+ int bm = 0;
+ if ( modifications.containsKey(name)) {
+ bm = toInt(modifications.get(name));
+ } else {
+ bm = toInt(currentAcl.get(name));
+ }
+ return bm;
+ }
+
+ private String inverseKeyOf(String key) {
+ if (key == null) {
+ return null;
+ }
+ if (AclModification.isGrant(key)) {
+ return AclModification.getPrincipal(key) + AclModification.DENIED_MARKER;
+ } else if (AclModification.isDeny(key)) {
+ return AclModification.getPrincipal(key) + AclModification.GRANTED_MARKER;
+ } else {
+ return key;
+ }
+ }
+
+ public void check(String objectType, String objectPath, Permission permission)
+ throws AccessDeniedException, StorageClientException {
+ if (user.isAdmin()) {
+ return;
+ }
+ if ( compilingPermissions.isSet() ) {
+ return;
+ }
+ // users can always operate on their own user object.
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType) && user.getId().equals(objectPath)) {
+ return;
+ }
+ int[] privileges = compilePermission(user, objectType, objectPath, 0);
+ if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
+ throw new AccessDeniedException(objectType, objectPath, permission.getName(),
+ user.getId());
+ }
+ }
+
+
+ private String getAclKey(String objectType, String objectPath) {
+ return objectType + ";" + objectPath;
+ }
+
+ public void setRequestPrincipalResolver(PrincipalTokenResolver principalTokenResolver ) {
+ this.principalTokenResolver = principalTokenResolver;
+ }
+ public void clearRequestPrincipalResolver() {
+ principalTokenResolver = null;
+ }
+
+ private int[] compilePermission(Authorizable authorizable, String objectType,
+ String objectPath, int recursion) throws StorageClientException {
+ String key = getAclKey(objectType, objectPath);
+ if (user.getId().equals(authorizable.getId()) && cache.containsKey(key)) {
+ return cache.get(key);
+ } else {
+ LOGGER.debug("Cache Miss {} [{}] ", cache, key);
+ }
+ try {
+ // we need to allow the permissions compile to bypass access control as it needs to see everything.
+ compilingPermissions.inc();
+ Map acl = getCached(keySpace, aclColumnFamily, key);
+ LOGGER.debug("ACL on {} is {} ", key, acl);
+
+ int grants = 0;
+ int denies = 0;
+ if (acl != null) {
+
+ {
+ String principal = authorizable.getId();
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl
+ .get(principal + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{principal,tg,td,grants,denies});
+
+ }
+ /*
+ * Deal with any proxy principals, these override groups
+ */
+ if (principalTokenResolver != null) {
+ Set inspected = Sets.newHashSet();
+ if ( acl.containsKey(_SECRET_KEY)) {
+ String secretKey = (String) acl.get(_SECRET_KEY);
+ if ( secretKey != null ) {
+ for (Entry ace : acl.entrySet()) {
+ String k = ace.getKey();
+ LOGGER.debug("Checking {} ",k);
+ if (k.startsWith(DYNAMIC_PRINCIPAL_STEM)) {
+ String proxyPrincipal = AclModification.getPrincipal(k).substring(DYNAMIC_PRINCIPAL_STEM.length());
+ if ( !inspected.contains(proxyPrincipal)) {
+ inspected.add(proxyPrincipal);
+ LOGGER.debug("Is Dynamic {}, checking ",k);
+ try {
+ // principalTokenValidators are not safe code, hence we must re-enable full access control.
+ compilingPermissions.suspend();
+ List proxyPrincipalTokens = principalTokenResolver.resolveTokens(proxyPrincipal);
+ for ( Content proxyPrincipalToken : proxyPrincipalTokens ) {
+ if ( principalTokenValidator.validatePrincipal(proxyPrincipalToken, secretKey)) {
+ String pname = DYNAMIC_PRINCIPAL_STEM+proxyPrincipal;
+ LOGGER.debug("Has this principal {} ", proxyPrincipal);
+ int tg = toInt(acl.get(pname
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl.get(pname
+ + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{pname, tg,td,grants,denies});
+ break;
+ }
+ }
+ } finally {
+ // when done, we must resume compiling permissions where we were.
+ // NB, the code is re-entrant.
+ compilingPermissions.resume();
+ }
+ }
+ }
+ }
+ } else {
+ LOGGER.debug("Secret Key is null");
+ }
+ } else {
+ LOGGER.debug("No Secret Key Key ");
+ }
+ } else {
+ LOGGER.debug("No principalToken Resolver");
+ }
+ // then deal with static principals
+ for (String principal : getPrincipals(authorizable) ) {
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl
+ .get(principal + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{principal,tg,td,grants,denies});
+ }
+
+ // Everyone must be the last principal to be applied
+ if (!User.ANON_USER.equals(authorizable.getId())) {
+ // all users except anon are in the group everyone, by default
+ // but only if not already denied or granted by a more specific
+ // permission.
+ int tg = (toInt(acl.get(Group.EVERYONE
+ + AclModification.GRANTED_MARKER)) & ~denies);
+ int td = (toInt(acl.get(Group.EVERYONE
+ + AclModification.DENIED_MARKER)) & ~grants);
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{Group.EVERYONE,tg,td,grants,denies});
+
+ }
+ /*
+ * grants contains the granted permissions in a bitmap denies
+ * contains the denied permissions in a bitmap
+ */
+ int granted = grants;
+ int denied = denies;
+
+ /*
+ * Only look to parent objects if this is not the root object and
+ * everything is not granted and denied
+ */
+ if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)
+ && (granted != 0xffff || denied != 0xffff)) {
+ recursion++;
+ int[] parentPriv = compilePermission(authorizable, objectType,
+ StorageClientUtils.getParentObjectPath(objectPath), recursion);
+ if (parentPriv != null) {
+ /*
+ * Grant permission not denied at this level parentPriv[0]
+ * is permissions granted by the parent ~denies is
+ * permissions not denied here parentPriv[0] & ~denies is
+ * permissions granted by the parent that have not been
+ * denied here. we need to add those to things granted here.
+ * ie |
+ */
+ granted = grants | (parentPriv[0] & ~denies);
+ /*
+ * Deny permissions not granted at this level
+ */
+ denied = denies | (parentPriv[1] & ~grants);
+ }
+ }
+ // If not denied all users and groups can read other users and
+ // groups and all content can be read
+ if (((denied & Permissions.CAN_READ.getPermission()) == 0)
+ && (Security.ZONE_AUTHORIZABLES.equals(objectType) || Security.ZONE_CONTENT
+ .equals(objectType))) {
+ granted = granted | Permissions.CAN_READ.getPermission();
+ LOGGER.debug("Default Read Permission set {} {} ",key,denied);
+ } else {
+ LOGGER.debug("Default Read has been denied {} {} ",key,
+ denied);
+ }
+ LOGGER.debug("Permissions on {} for {} is {} {} ",new
+ Object[]{key,user.getId(),granted,denied});
+ /*
+ * Keep a cached copy
+ */
+ if (user.getId().equals(authorizable.getId())) {
+ cache.put(key, new int[] { granted, denied });
+ }
+ return new int[] { granted, denied };
+
+ }
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType)
+ || Security.ZONE_CONTENT.equals(objectType)) {
+ // unless explicitly denied all users can read other users.
+ return new int[] { Permissions.CAN_READ.getPermission(), 0 };
+ }
+ return new int[] { 0, 0 };
+ } finally {
+ // decrement the counter from here.
+ compilingPermissions.dec();
+ }
+ }
+
+
+ private String[] getPrincipals(final Authorizable authorizable) {
+ String k = authorizable.getId();
+ if (principalCache.containsKey(k)) {
+ return principalCache.get(k);
+ }
+ Set memberOfSet = Sets.newHashSet(authorizable.getPrincipals());
+ if ( authorizableManager != null ) {
+ // membership resolution is possible, but we had better turn off recursion
+ if ( principalRecursionLock.get() == null ) {
+ principalRecursionLock.set("l");
+ try {
+ for ( Iterator gi = authorizable.memberOf(authorizableManager); gi.hasNext(); ) {
+ memberOfSet.add(gi.next().getId());
+ }
+ } finally {
+ principalRecursionLock.set(null);
+ }
+ }
+ }
+ memberOfSet.remove(Group.EVERYONE);
+ String[] m = memberOfSet.toArray(new String[memberOfSet.size()]);
+ principalCache.put(k, m);
+ return m;
+ }
+
+
+ private int toInt(Object object) {
+ if ( object instanceof Integer ) {
+ return ((Integer) object).intValue();
+ }
+ LOGGER.debug("Bitmap Not Present");
+ return 0;
+ }
+
+ public String getCurrentUserId() {
+ return user.getId();
+ }
+
+ public void close() {
+ closed = true;
+ }
+
+ private void checkOpen() throws StorageClientException {
+ if (closed) {
+ throw new StorageClientException("Access Control Manager is closed");
+ }
+ }
+
+ public boolean can(Authorizable authorizable, String objectType, String objectPath,
+ Permission permission) {
+ if ( compilingPermissions.isSet() ) {
+ return true;
+ }
+ if (authorizable instanceof User && ((User) authorizable).isAdmin()) {
+ return true;
+ }
+ // users can always operate on their own user object.
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType)
+ && authorizable.getId().equals(objectPath)) {
+ return true;
+ }
+ try {
+ int[] privileges = compilePermission(authorizable, objectType, objectPath, 0);
+ if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
+ return false;
+ }
+ } catch (StorageClientException e) {
+ LOGGER.warn(e.getMessage(), e);
+ return false;
+ }
+ return true;
+ }
+
+ public Permission[] getPermissions(String objectType, String path) throws StorageClientException {
+ int[] perms = compilePermission(this.user, objectType, path, 0);
+ List permissions = Lists.newArrayList();
+ for (Permission p : Permissions.PRIMARY_PERMISSIONS) {
+ if ((perms[0] & p.getPermission()) == p.getPermission()) {
+ permissions.add(p);
+ }
+ }
+ return permissions.toArray(new Permission[permissions.size()]);
+ }
+
+ public String[] findPrincipals(String objectType, String objectPath, int permission, boolean granted) throws StorageClientException {
+ Map principalMap = internalCompilePrincipals(objectType, objectPath, 0);
+ LOGGER.debug("Got Principals {} ",principalMap);
+ List principals = Lists.newArrayList();
+ for (Entry perm : principalMap.entrySet()) {
+ int[] p = perm.getValue();
+ if ( granted && (p[0] & permission) == permission ) {
+ principals.add(perm.getKey());
+ LOGGER.debug("Included {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ } else if ( !granted && (p[1] & permission) == permission) {
+ principals.add(perm.getKey());
+ LOGGER.debug("Included {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ } else {
+ LOGGER.debug("Filtered {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ }
+ }
+ LOGGER.debug(" Found Principals {} ",principals);
+ return principals.toArray(new String[principals.size()]);
+ }
+
+
+
+ private Map internalCompilePrincipals(String objectType, String objectPath, int recursion) throws StorageClientException {
+ Map compiledPermissions = Maps.newHashMap();
+ String key = getAclKey(objectType, objectPath);
+
+ Map acl = getCached(keySpace, aclColumnFamily, key);
+
+ if (acl != null) {
+ LOGGER.debug("Checking {} {} ",key,acl);
+ for (Entry ace : acl.entrySet()) {
+ String aceKey = ace.getKey();
+ String principal = aceKey.substring(0, aceKey.length() - 2);
+
+ if (!compiledPermissions.containsKey(principal)) {
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl.get(principal
+ + AclModification.DENIED_MARKER));
+ compiledPermissions.put(principal, new int[] { tg, td });
+ LOGGER.debug("added {} ",principal);
+ }
+
+ }
+ }
+ /*
+ * grants contains the granted permissions in a bitmap denies contains
+ * the denied permissions in a bitmap
+ */
+
+ /*
+ * Only look to parent objects if this is not the root object and
+ * everything is not granted and denied
+ */
+ if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)) {
+ recursion++;
+ Map parentPermissions = internalCompilePrincipals(objectType,
+ StorageClientUtils.getParentObjectPath(objectPath), recursion);
+ // add the parernt privileges in
+ for (Entry parentPermission : parentPermissions.entrySet()) {
+ int[] thisPriv = new int[2];
+ String principal = parentPermission.getKey();
+ if (compiledPermissions.containsKey(principal)) {
+ thisPriv = compiledPermissions.get(principal);
+ LOGGER.debug("modified {} ",principal);
+ } else {
+ LOGGER.debug("creating {} ",principal);
+ }
+ int[] parentPriv = parentPermission.getValue();
+
+ /*
+ * Grant permission not denied at this level parentPriv[0] is
+ * permissions granted by the parent ~denies is permissions not
+ * denied here parentPriv[0] & ~denies is permissions granted by
+ * the parent that have not been denied here. we need to add
+ * those to things granted here. ie |
+ */
+ int granted = thisPriv[0] | (parentPriv[0] & ~thisPriv[1]);
+ /*
+ * Deny permissions not granted at this level
+ */
+ int denied = thisPriv[1] | (parentPriv[1] & ~thisPriv[0]);
+
+ compiledPermissions.put(principal, new int[] { granted, denied });
+
+ }
+ }
+
+ //
+ // If not denied all users and groups can read other users and
+ // groups and all content can be read
+ for (String principal : new String[] { Group.EVERYONE, User.ANON_USER }) {
+ int[] perm = new int[2];
+ if (compiledPermissions.containsKey(principal)) {
+ perm = compiledPermissions.get(principal);
+ }
+ if (((perm[1] & Permissions.CAN_READ.getPermission()) == 0)
+ && (Security.ZONE_AUTHORIZABLES.equals(objectType) || Security.ZONE_CONTENT
+ .equals(objectType))) {
+ perm[0] = perm[0] | Permissions.CAN_READ.getPermission();
+ LOGGER.debug("added Default {} ",principal);
+ compiledPermissions.put(principal, perm);
+ }
+ }
+ compiledPermissions.put(User.ADMIN_USER, new int[] { 0xffff, 0x0000});
+ return compiledPermissions;
+ // only store those permissions the match the requested set.]
+
+
+ }
+
+ @Override
+ protected Logger getLogger() {
+ return LOGGER;
+ }
+
+ public void setAuthorizableManager(AuthorizableManager authorizableManager) {
+ this.authorizableManager = authorizableManager;
+ }
+
+
+
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java
new file mode 100644
index 00000000..5681d6e3
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+
+public class AccessControlledMap extends HashMap {
+
+
+ private PropertyAcl propertyAcl;
+
+ public AccessControlledMap(PropertyAcl propertyAcl) {
+ this.propertyAcl = propertyAcl;
+ }
+ /**
+ *
+ */
+ private static final long serialVersionUID = -6550830558631198709L;
+
+ @Override
+ public V put(K key, V value) {
+ if ( propertyAcl.canWrite(key)) {
+ return super.put(key, value);
+ }
+ return null;
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ for ( Entry extends K, ? extends V> e : m.entrySet()) {
+ put(e.getKey(), e.getValue());
+ }
+ }
+
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
similarity index 61%
rename from src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
index 96260f71..5d80c524 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
@@ -17,35 +17,37 @@
*/
package org.sakaiproject.nakamura.lite.accesscontrol;
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Authenticator;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
+import org.sakaiproject.nakamura.api.lite.util.EnabledPeriod;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
import org.sakaiproject.nakamura.lite.authorizable.UserInternal;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Map;
-
-public class AuthenticatorImpl implements Authenticator {
+public class AuthenticatorImpl extends CachingManagerImpl implements Authenticator {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticatorImpl.class);
- private StorageClient client;
private String keySpace;
private String authorizableColumnFamily;
- public AuthenticatorImpl(StorageClient client, Configuration configuration) {
- this.client = client;
+ public AuthenticatorImpl(StorageClient client, Configuration configuration, Map sharedCache) {
+ super(client, sharedCache);
this.keySpace = configuration.getKeySpace();
this.authorizableColumnFamily = configuration.getAuthorizableColumnFamily();
}
public User authenticate(String userid, String password) {
try {
- Map userAuthMap = client
- .get(keySpace, authorizableColumnFamily, userid);
+ Map userAuthMap = getCached(keySpace, authorizableColumnFamily, userid);
if (userAuthMap == null) {
LOGGER.debug("User was not found {}", userid);
return null;
@@ -55,29 +57,48 @@ public User authenticate(String userid, String password) {
String storedPassword = (String) userAuthMap
.get(User.PASSWORD_FIELD);
if (passwordHash.equals(storedPassword)) {
- return new UserInternal(userAuthMap, false);
+ if ( EnabledPeriod.isInEnabledPeriod((String) userAuthMap.get(User.LOGIN_ENABLED_PERIOD_FIELD)) ) {
+ return new UserInternal(userAuthMap, null, false);
+ }
}
LOGGER.debug("Failed to authentication, passwords did not match");
} catch (StorageClientException e) {
LOGGER.debug("Failed To authenticate " + e.getMessage(), e);
+ } catch (AccessDeniedException e) {
+ LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
}
return null;
}
-
public User systemAuthenticate(String userid) {
+ return internalSystemAuthenticate(userid, false);
+ }
+ public User systemAuthenticateBypassEnable(String userid) {
+ return internalSystemAuthenticate(userid, true);
+ }
+
+ private User internalSystemAuthenticate(String userid, boolean forceEnableLogin) {
try {
- Map userAuthMap = client
- .get(keySpace, authorizableColumnFamily, userid);
+ Map userAuthMap = getCached(keySpace, authorizableColumnFamily, userid);
if (userAuthMap == null || userAuthMap.size() == 0) {
LOGGER.debug("User was not found {}", userid);
return null;
}
- return new UserInternal(userAuthMap, false);
+ if ( forceEnableLogin || EnabledPeriod.isInEnabledPeriod((String) userAuthMap.get(User.LOGIN_ENABLED_PERIOD_FIELD)) ) {
+ return new UserInternal(userAuthMap, null, false);
+ }
} catch (StorageClientException e) {
LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
+ } catch (AccessDeniedException e) {
+ LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
}
return null;
}
+ @Override
+ protected Logger getLogger() {
+ return LOGGER;
+ }
+
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java
new file mode 100644
index 00000000..8c7a5e44
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+
+public class DefaultPrincipalValidator implements PrincipalValidatorPlugin {
+
+ public boolean validate(Content proxyPrincipalToken) {
+ // TODO add some standard validation steps like date
+ return true;
+ }
+
+ public String[] getProtectedFields() {
+ return new String[0];
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java
new file mode 100644
index 00000000..7faa6cae
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import org.apache.commons.codec.binary.Base64;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PrincipalTokenValidator {
+
+
+ public static final String VALIDATORPLUGIN = "validatorplugin";
+ public static final String _ACLTOKEN = "_acltoken";
+ private static final String HMAC_SHA512 = "HmacSHA512";
+ private static final Logger LOGGER = LoggerFactory.getLogger(PrincipalTokenValidator.class);
+ private PrincipalValidatorPlugin defaultPrincipalValidator = new DefaultPrincipalValidator();
+ private PrincipalValidatorResolver principalValidatorResolver;
+
+ public PrincipalTokenValidator(PrincipalValidatorResolver principalValidatorResolver) {
+ this.principalValidatorResolver = principalValidatorResolver;
+ }
+
+ public boolean validatePrincipal(Content proxyPrincipalToken, String sharedKey) {
+ if ( proxyPrincipalToken == null) {
+ LOGGER.debug("Failed to Validate Token at no content item ");
+ return false;
+ }
+ if ( !proxyPrincipalToken.hasProperty(_ACLTOKEN)) {
+ LOGGER.debug("Failed to Validate Token at {} no ACL Token ", proxyPrincipalToken.getPath());
+ return false;
+ }
+ PrincipalValidatorPlugin plugin = null;
+ if ( proxyPrincipalToken.hasProperty(VALIDATORPLUGIN) ) {
+ plugin = principalValidatorResolver.getPluginByName((String) proxyPrincipalToken.getProperty(VALIDATORPLUGIN));
+ } else {
+ plugin = defaultPrincipalValidator;
+ }
+ if ( plugin == null ) {
+ LOGGER.debug("Failed to Validate Token at {} no plugin ");
+ return false;
+ }
+ String hmac = signToken(proxyPrincipalToken, sharedKey, plugin);
+ if ( hmac == null || !hmac.equals(proxyPrincipalToken.getProperty(_ACLTOKEN)) ) {
+ LOGGER.debug("Failed to Validate Token at {} as {}, does not match ",proxyPrincipalToken.getPath(), hmac);
+ return false;
+ }
+ boolean validate = plugin.validate(proxyPrincipalToken);
+ if ( validate ) {
+ LOGGER.debug("Validated Token at {} as {} using plugin {} ",new Object[] { proxyPrincipalToken.getPath(), hmac, plugin});
+ } else {
+ LOGGER.debug("Invalid Token at {} as {} using plugin {} ",new Object[] { proxyPrincipalToken.getPath(), hmac, plugin});
+ }
+ return validate;
+ }
+
+ public void signToken(Content token, String sharedKey ) throws StorageClientException {
+ PrincipalValidatorPlugin plugin = null;
+ if ( token.hasProperty(VALIDATORPLUGIN) ) {
+ plugin = principalValidatorResolver.getPluginByName((String) token.getProperty(VALIDATORPLUGIN));
+ } else {
+ plugin = defaultPrincipalValidator;
+ }
+ if ( plugin == null ) {
+ throw new StorageClientException("The property validatorplugin does not specify an active PricipalValidatorPlugin, cant sign");
+ }
+ token.setProperty(_ACLTOKEN, signToken(token, sharedKey, plugin));
+ }
+
+ private String signToken(Content token, String sharedKey, PrincipalValidatorPlugin plugin) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-512");
+ byte[] input = sharedKey.getBytes("UTF-8");
+ byte[] data = md.digest(input);
+ SecretKeySpec key = new SecretKeySpec(data, HMAC_SHA512);
+ return getHmac(token, plugin.getProtectedFields(), key);
+ } catch (InvalidKeyException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (IllegalStateException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ }
+ }
+
+
+ private String getHmac(Content principalToken, String[] extraFields, SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {
+ StringBuilder sb = new StringBuilder();
+ sb.append(principalToken.getPath()).append("@");
+ if ( principalToken.hasProperty(VALIDATORPLUGIN)) {
+ sb.append(principalToken.getProperty(VALIDATORPLUGIN)).append("@");
+ }
+ for (String f : extraFields) {
+ if ( principalToken.hasProperty(f)) {
+ sb.append(principalToken.getProperty(f)).append("@");
+ } else {
+ sb.append("null").append("@");
+ }
+ }
+ Mac m = Mac.getInstance(HMAC_SHA512);
+ m.init(key);
+ String message = sb.toString();
+ LOGGER.debug("Signing {} ", message);
+ m.update(message.getBytes("UTF-8"));
+ return Base64.encodeBase64URLSafeString(m.doFinal());
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java
new file mode 100644
index 00000000..b4ad2444
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import com.google.common.collect.Maps;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+
+import java.util.Map;
+
+@Component(immediate=true, metatype=true, enabled=true)
+@Service(value=PrincipalValidatorResolver.class)
+public class PrincipalValidatorResolverImpl implements PrincipalValidatorResolver {
+
+ protected Map pluginStore = Maps.newConcurrentMap();
+
+ public PrincipalValidatorPlugin getPluginByName(String key) {
+ return pluginStore.get(key);
+ }
+
+ public void registerPlugin(String key, PrincipalValidatorPlugin plugin) {
+ pluginStore.put(key, plugin);
+ }
+ public void unregisterPlugin(String key) {
+ pluginStore.remove(key);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java
new file mode 100644
index 00000000..deab4bb4
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+public class PropertyAcl implements Serializable {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -3998584870894631478L;
+ private Set readDenied;
+ private Set writeDenied;
+
+ public PropertyAcl(Map denies) {
+ Set r = Sets.newHashSet();
+ Set w = Sets.newHashSet();
+ for (Entry ace : denies.entrySet()) {
+ if ((Permissions.CAN_READ_PROPERTY.getPermission() & ace.getValue()) == Permissions.CAN_READ_PROPERTY
+ .getPermission()) {
+ r.add(ace.getKey());
+ }
+ if ((Permissions.CAN_WRITE_PROPERTY.getPermission() & ace.getValue()) == Permissions.CAN_WRITE_PROPERTY
+ .getPermission()) {
+ w.add(ace.getKey());
+ }
+ }
+ readDenied = ImmutableSet.copyOf(r.toArray(new String[r.size()]));
+ writeDenied = ImmutableSet.copyOf(w.toArray(new String[w.size()]));
+ }
+
+ public PropertyAcl() {
+ readDenied = ImmutableSet.of();
+ writeDenied = ImmutableSet.of();
+ }
+
+ public Set readDeniedSet() {
+ return readDenied;
+ }
+
+ public boolean canWrite(Object key) {
+ return !writeDenied.contains(key);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java
new file mode 100644
index 00000000..428a2701
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Maintains a thread bound reference counter that can be suspended and resumed.
+ * When suspended the current counter us pushed to a stack, and the new counter
+ * is started. When resumed, the current counter is replaced with the counter on
+ * the stack. The operations are all bound to the current thread. inc() dec()
+ * and suspend() resume() should be used in matching pairs protected by try {
+ * ... } finally { ... } constructs. The class makes no attempt to guess what
+ * the code is doing.
+ *
+ * @author ieb
+ *
+ */
+public class ThreadBoundStackReferenceCounter {
+
+ // dont use initial value to avoid JVM bugs.
+ private ThreadLocal counter = new ThreadLocal();
+ private ThreadLocal> suspended = new ThreadLocal>();
+
+ public void inc() {
+ set(get() + 1);
+ }
+
+ public void dec() {
+ set(get() - 1);
+ }
+
+ public void suspend() {
+ push(get());
+ set(0);
+ }
+
+ public void resume() {
+ set(pop());
+ }
+
+ public boolean isSet() {
+ return get() > 0;
+ }
+
+ private int get() {
+ Integer c = counter.get();
+ if (c == null) {
+ return 0;
+ }
+ return c.intValue();
+ }
+
+ private void set(int i) {
+ if (i < 0) {
+ i = 0;
+ }
+ counter.set(i);
+ }
+
+ private void push(int i) {
+ List s = suspended.get();
+ if (s == null) {
+ s = Lists.newArrayList();
+ suspended.set(s);
+ }
+ s.add(i);
+ }
+
+ private int pop() {
+ List s = suspended.get();
+ if (s == null) {
+ s = Lists.newArrayList();
+ suspended.set(s);
+ }
+ if (s.size() == 0) {
+ return 0;
+ }
+ return s.remove(s.size() - 1);
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
similarity index 95%
rename from src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
index e5fe9640..b982287f 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
@@ -25,7 +25,7 @@
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -77,7 +77,7 @@ private void createSystemUser() throws StorageClientException {
User.SYSTEM_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.SYSTEM_USER, Authorizable.NAME_FIELD,
+ (Object)User.SYSTEM_USER, Authorizable.NAME_FIELD,
User.SYSTEM_USER, Authorizable.PASSWORD_FIELD,
"--no-password--",
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
@@ -94,7 +94,7 @@ private void createAdminUser() throws StorageClientException {
User.ADMIN_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.ADMIN_USER, Authorizable.NAME_FIELD,
+ (Object)User.ADMIN_USER, Authorizable.NAME_FIELD,
User.ADMIN_USER, Authorizable.PASSWORD_FIELD,
StorageClientUtils.secureHash("admin"),
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
@@ -110,7 +110,7 @@ private void createAnonUser() throws StorageClientException {
User.ANON_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.ANON_USER, Authorizable.NAME_FIELD,
+ (Object)User.ANON_USER, Authorizable.NAME_FIELD,
User.ANON_USER, Authorizable.PASSWORD_FIELD,
Authorizable.NO_PASSWORD,
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
similarity index 63%
rename from src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
index 4fedb9b7..7ffc0e42 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
@@ -17,13 +17,14 @@
*/
package org.sakaiproject.nakamura.lite.authorizable;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.Session;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
import org.sakaiproject.nakamura.api.lite.StoreListener;
@@ -37,16 +38,20 @@
import org.sakaiproject.nakamura.api.lite.authorizable.Group;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.api.lite.util.PreemptiveIterator;
-import org.sakaiproject.nakamura.lite.CachingManager;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
+import org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.DisposableIterator;
+import org.sakaiproject.nakamura.lite.storage.spi.SparseRow;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
/**
* An Authourizable Manager bound to a user, on creation the user ID specified
@@ -55,11 +60,16 @@
* @author ieb
*
*/
-public class AuthorizableManagerImpl extends CachingManager implements AuthorizableManager {
+public class AuthorizableManagerImpl extends CachingManagerImpl implements AuthorizableManager {
+ private static final String DISABLED_PASSWORD_HASH = "--disabled--";
private static final Set FILTER_ON_UPDATE = ImmutableSet.of(Authorizable.ID_FIELD,
- Authorizable.PASSWORD_FIELD);
+ Authorizable.PASSWORD_FIELD, Authorizable.LOGIN_ENABLED_PERIOD_FIELD);
private static final Set FILTER_ON_CREATE = ImmutableSet.of(Authorizable.ID_FIELD,
+ Authorizable.PASSWORD_FIELD, Authorizable.LOGIN_ENABLED_PERIOD_FIELD);
+ private static final Set ADMIN_FILTER_ON_UPDATE = ImmutableSet.of(Authorizable.ID_FIELD,
+ Authorizable.PASSWORD_FIELD);
+ private static final Set ADMIN_FILTER_ON_CREATE = ImmutableSet.of(Authorizable.ID_FIELD,
Authorizable.PASSWORD_FIELD);
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizableManagerImpl.class);
private String currentUserId;
@@ -71,9 +81,12 @@ public class AuthorizableManagerImpl extends CachingManager implements Authoriza
private boolean closed;
private Authenticator authenticator;
private StoreListener storeListener;
+ private Session session;
+ private Set filterOnUpdate;
+ private Set filterOnCreate;
- public AuthorizableManagerImpl(User currentUser, StorageClient client,
- Configuration configuration, AccessControlManager accessControlManager,
+ public AuthorizableManagerImpl(User currentUser, Session session, StorageClient client,
+ Configuration configuration, AccessControlManagerImpl accessControlManager,
Map sharedCache, StoreListener storeListener) throws StorageClientException,
AccessDeniedException {
super(client, sharedCache);
@@ -82,13 +95,22 @@ public AuthorizableManagerImpl(User currentUser, StorageClient client,
throw new RuntimeException("Current User ID shoud not be null");
}
this.thisUser = currentUser;
+ if ( thisUser.isAdmin() ) {
+ filterOnUpdate = ADMIN_FILTER_ON_UPDATE;
+ filterOnCreate = ADMIN_FILTER_ON_CREATE;
+ } else {
+ filterOnUpdate = FILTER_ON_UPDATE;
+ filterOnCreate = FILTER_ON_CREATE;
+ }
+ this.session = session;
this.client = client;
this.accessControlManager = accessControlManager;
this.keySpace = configuration.getKeySpace();
this.authorizableColumnFamily = configuration.getAuthorizableColumnFamily();
- this.authenticator = new AuthenticatorImpl(client, configuration);
+ this.authenticator = new AuthenticatorImpl(client, configuration, sharedCache);
this.closed = false;
this.storeListener = storeListener;
+ accessControlManager.setAuthorizableManager(this);
}
public User getUser() {
@@ -112,18 +134,40 @@ public Authorizable findAuthorizable(final String authorizableId) throws AccessD
return null;
}
if (isAUser(authorizableMap)) {
- return new UserInternal(authorizableMap, false);
+ return new UserInternal(authorizableMap, session, false);
} else if (isAGroup(authorizableMap)) {
- return new GroupInternal(authorizableMap, false);
+ return new GroupInternal(authorizableMap, session, false);
}
return null;
}
public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedException,
StorageClientException {
+ updateAuthorizable(authorizable, true);
+ }
+
+ public void updateAuthorizable(Authorizable authorizable, boolean withTouch) throws AccessDeniedException,
+ StorageClientException {
checkOpen();
+ if ( !withTouch && !thisUser.isAdmin() ) {
+ throw new StorageClientException("Only admin users can update without touching the user");
+ }
String id = authorizable.getId();
+ if ( authorizable.isImmutable() ) {
+ throw new StorageClientException("You cant update an immutable authorizable:"+id);
+ }
+ if ( authorizable.isReadOnly() ) {
+ return;
+ }
+ if ( authorizable.isNew() ) {
+ throw new StorageClientException("You must create an authorizable if its new, you cant update an new authorizable");
+ }
accessControlManager.check(Security.ZONE_AUTHORIZABLES, id, Permissions.CAN_WRITE);
+ if ( !authorizable.isModified() ) {
+ return;
+ // only perform the update and send the event if we see the authorizable as modified. It will be modified ig group membership was changed.
+ }
+
/*
* Update the principal records for members. The list of members that
* have been added and removed is converted into a list of Authorzables.
@@ -138,10 +182,14 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
* permission at some point in the future.
*/
String type = "type:user";
+ List attributes = Lists.newArrayList();
+ String[] membersAdded = null;
+ String[] membersRemoved = null;
+
if (authorizable instanceof Group) {
type = "type:group";
Group group = (Group) authorizable;
- String[] membersAdded = group.getMembersAdded();
+ membersAdded = group.getMembersAdded();
Authorizable[] newMembers = new Authorizable[membersAdded.length];
int i = 0;
for (String newMember : membersAdded) {
@@ -166,7 +214,7 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
i++;
}
i = 0;
- String[] membersRemoved = group.getMembersRemoved();
+ membersRemoved = group.getMembersRemoved();
Authorizable[] retiredMembers = new Authorizable[membersRemoved.length];
for (String retiredMember : membersRemoved) {
try {
@@ -181,7 +229,9 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
}
- LOGGER.debug("Membership Change added [{}] removed [{}] ", Arrays.toString(newMembers), Arrays.toString(retiredMembers));
+ String membersAddedCsv = StringUtils.join(membersAdded, ',');
+ String membersRemovedCsv = StringUtils.join(membersRemoved, ',');
+ LOGGER.debug("Membership Change added [{}] removed [{}] ", membersAddedCsv, membersRemovedCsv);
int changes = 0;
// there is now a sparse list of authorizables, that need changing
for (Authorizable newMember : newMembers) {
@@ -190,7 +240,8 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
if (newMember.isModified()) {
Map encodedProperties = StorageClientUtils
.getFilteredAndEcodedMap(newMember.getPropertiesForUpdate(),
- FILTER_ON_UPDATE);
+ filterOnUpdate);
+ encodedProperties.put(Authorizable.ID_FIELD, newMember.getId());
putCached(keySpace, authorizableColumnFamily, newMember.getId(),
encodedProperties, newMember.isNew());
LOGGER.debug("Updated {} with principal {} {} ",new Object[]{newMember.getId(), group.getId(), encodedProperties});
@@ -208,7 +259,8 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
if (retiredMember.isModified()) {
Map encodedProperties = StorageClientUtils
.getFilteredAndEcodedMap(retiredMember.getPropertiesForUpdate(),
- FILTER_ON_UPDATE);
+ filterOnUpdate);
+ encodedProperties.put(Authorizable.ID_FIELD, retiredMember.getId());
putCached(keySpace, authorizableColumnFamily, retiredMember.getId(),
encodedProperties, retiredMember.isNew());
changes++;
@@ -220,24 +272,56 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
}
}
LOGGER.debug(" Finished Updating other principals, made {} changes, Saving Changes to {} ", changes, id);
- }
+ // if there were added or removed members, send them out as event properties for
+ // external integration
+ if (membersAdded.length > 0) {
+ attributes.add("added:" + membersAddedCsv);
+ }
+ if (membersRemoved.length > 0) {
+ attributes.add("removed:" + membersRemovedCsv);
+ }
+ }
+ attributes.add(type);
+ boolean wasNew = authorizable.isNew();
+ Map beforeUpdateProperties = authorizable.getOriginalProperties();
Map encodedProperties = StorageClientUtils.getFilteredAndEcodedMap(
- authorizable.getPropertiesForUpdate(), FILTER_ON_UPDATE);
- encodedProperties.put(Authorizable.LASTMODIFIED,System.currentTimeMillis());
- encodedProperties.put(Authorizable.LASTMODIFIED_BY,accessControlManager.getCurrentUserId());
+ authorizable.getPropertiesForUpdate(), filterOnUpdate);
+ if (withTouch) {
+ encodedProperties.put(Authorizable.LASTMODIFIED_FIELD, System.currentTimeMillis());
+ encodedProperties.put(Authorizable.LASTMODIFIED_BY_FIELD,
+ accessControlManager.getCurrentUserId());
+ }
+ encodedProperties.put(Authorizable.ID_FIELD, id); // make certain the ID is always there.
putCached(keySpace, authorizableColumnFamily, id, encodedProperties, authorizable.isNew());
authorizable.reset(getCached(keySpace, authorizableColumnFamily, id));
- storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, accessControlManager.getCurrentUserId(), true, type);
+ String[] attrs = attributes.toArray(new String[attributes.size()]);
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, type, accessControlManager.getCurrentUserId(), wasNew, beforeUpdateProperties, attrs);
+ // for each added or removed member, send an UPDATE event so indexing can properly
+ // record the groups each member is a member of.\
+
+ // when we add members we dont emit an event with resource type in it.
+ if (membersAdded != null) {
+ for (String added : membersAdded) {
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, added, accessControlManager.getCurrentUserId(), null, false, null);
+ }
+ }
+ if (membersRemoved != null) {
+ for (String removed : membersRemoved) {
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, removed, accessControlManager.getCurrentUserId(), null, false, null);
+ }
+ }
}
+
public boolean createAuthorizable(String authorizableId, String authorizableName,
String password, Map properties) throws AccessDeniedException,
StorageClientException {
+ checkId(authorizableId);
if (properties == null) {
properties = Maps.newHashMap();
}
@@ -258,7 +342,7 @@ public boolean createAuthorizable(String authorizableId, String authorizableName
return false;
}
Map encodedProperties = StorageClientUtils.getFilteredAndEcodedMap(
- properties, FILTER_ON_CREATE);
+ properties, filterOnCreate);
encodedProperties.put(Authorizable.ID_FIELD, authorizableId);
encodedProperties
.put(Authorizable.NAME_FIELD, authorizableName);
@@ -269,14 +353,29 @@ public boolean createAuthorizable(String authorizableId, String authorizableName
encodedProperties.put(Authorizable.PASSWORD_FIELD,
Authorizable.NO_PASSWORD);
}
- encodedProperties.put(Authorizable.CREATED,
+ encodedProperties.put(Authorizable.CREATED_FIELD,
System.currentTimeMillis());
- encodedProperties.put(Authorizable.CREATED_BY,
+ encodedProperties.put(Authorizable.CREATED_BY_FIELD,
accessControlManager.getCurrentUserId());
putCached(keySpace, authorizableColumnFamily, authorizableId, encodedProperties, true);
return true;
}
+
+ private void checkId(String authorizableId) throws StorageClientException {
+ if ( authorizableId.charAt(0) == '_') {
+ throw new StorageClientException("Authorizables may not start with _ :"+authorizableId);
+ }
+ for ( int i = 0; i < authorizableId.length(); i++) {
+ int cp = authorizableId.codePointAt(i);
+ if ( Character.isWhitespace(cp) ||
+ Character.isISOControl(cp) ||
+ Character.isMirrored(cp) ) {
+ throw new StorageClientException("Authorizables may not contain :"+authorizableId.charAt(i));
+ }
+ }
+ }
+
public boolean createUser(String authorizableId, String authorizableName, String password,
Map properties) throws AccessDeniedException, StorageClientException {
if (properties == null) {
@@ -308,12 +407,39 @@ public boolean createGroup(String authorizableId, String authorizableName,
public void delete(String authorizableId) throws AccessDeniedException, StorageClientException {
checkOpen();
accessControlManager.check(Security.ZONE_ADMIN, authorizableId, Permissions.CAN_DELETE);
- removeFromCache(keySpace, authorizableColumnFamily, authorizableId);
- client.remove(keySpace, authorizableColumnFamily, authorizableId);
- storeListener.onDelete(Security.ZONE_AUTHORIZABLES, authorizableId, accessControlManager.getCurrentUserId());
+ Authorizable authorizable = findAuthorizable(authorizableId);
+ if (authorizable != null){
+ removeCached(keySpace, authorizableColumnFamily, authorizableId);
+ storeListener.onDelete(Security.ZONE_AUTHORIZABLES, authorizableId, accessControlManager.getCurrentUserId(), getType(authorizable), authorizable.getOriginalProperties());
+ }
+ }
+
+ private String getType(Authorizable authorizable) {
+ if ( authorizable != null ) {
+ if ( authorizable.hasProperty(Authorizable.AUTHORIZABLE_TYPE_FIELD)) {
+ return (String) authorizable.getProperty(Authorizable.AUTHORIZABLE_TYPE_FIELD);
+ } else if ( authorizable instanceof Group) {
+ return Authorizable.GROUP_VALUE;
+ } else if ( authorizable instanceof User) {
+ // this was an object.
+ return String.valueOf(Authorizable.USER_VALUE);
+ }
+
+ }
+ return null;
+ }
+ private String getType(Map props) {
+ if ( props != null ) {
+ if ( props.containsKey(Authorizable.AUTHORIZABLE_TYPE_FIELD)) {
+ return (String) props.get(Authorizable.AUTHORIZABLE_TYPE_FIELD);
+ }
+ }
+ return null;
}
+
+
public void close() {
closed = true;
}
@@ -333,19 +459,21 @@ public void changePassword(Authorizable authorizable, String password, String ol
if (!thisUser.isAdmin()) {
User u = authenticator.authenticate(id, oldPassword);
if (u == null) {
- throw new StorageClientException(
+ throw new IllegalArgumentException(
"Unable to change passwords, old password does not match");
}
}
putCached(keySpace, authorizableColumnFamily, id, ImmutableMap.of(
- Authorizable.LASTMODIFIED,
+ Authorizable.LASTMODIFIED_FIELD,
(Object)System.currentTimeMillis(),
- Authorizable.LASTMODIFIED_BY,
+ Authorizable.ID_FIELD,
+ id,
+ Authorizable.LASTMODIFIED_BY_FIELD,
accessControlManager.getCurrentUserId(),
Authorizable.PASSWORD_FIELD,
StorageClientUtils.secureHash(password)), false);
- storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, currentUserId, false, "op:change-password");
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, currentUserId, getType(authorizable), false, null, "op:change-password");
} else {
throw new AccessDeniedException(Security.ZONE_ADMIN, id,
@@ -355,7 +483,7 @@ public void changePassword(Authorizable authorizable, String password, String ol
}
- public Iterator findAuthorizable(String propertyName, String value,
+ public DisposableIterator findAuthorizable(String propertyName, String value,
Class extends Authorizable> authorizableType) throws StorageClientException {
Builder builder = ImmutableMap.builder();
if (value != null) {
@@ -366,13 +494,14 @@ public Iterator findAuthorizable(String propertyName, String value
} else if (authorizableType.equals(Group.class)) {
builder.put(Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.GROUP_VALUE);
}
- final Iterator