SLING-5792 : API to manage Authentication Requirement. Add first implementation and add test based on patch provided by Angela Schreiber
git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1756370 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 545b9c0..92ca7fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,6 +125,12 @@
</dependency>
<dependency>
<groupId>org.osgi</groupId>
+ <artifactId>org.osgi.service.component</artifactId>
+ <version>1.3.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
<artifactId>org.osgi.service.event</artifactId>
<version>1.3.1</version>
<scope>provided</scope>
@@ -139,7 +145,10 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
-
+ <dependency>
+ <groupId>org.jmock</groupId>
+ <artifactId>jmock-junit4</artifactId>
+ </dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
@@ -153,8 +162,10 @@
<artifactId>junit</artifactId>
</dependency>
<dependency>
- <groupId>org.jmock</groupId>
- <artifactId>jmock-junit4</artifactId>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>1.10.19</version>
+ <scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@@ -166,5 +177,11 @@
<version>1.4</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>15.0</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java b/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
index f89b973..04d8c21 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
@@ -25,7 +25,7 @@
private final boolean requiresAuthentication;
static AuthenticationRequirementHolder fromConfig(final String config,
- final ServiceReference serviceReference) {
+ final ServiceReference<?> serviceReference) {
if (config == null || config.length() == 0) {
throw new IllegalArgumentException(
"Configuration must not be null or empty");
@@ -50,7 +50,7 @@
AuthenticationRequirementHolder(final String fullPath,
final boolean requiresAuthentication,
- final ServiceReference serviceReference) {
+ final ServiceReference<?> serviceReference) {
super(fullPath, serviceReference);
this.requiresAuthentication = requiresAuthentication;
}
diff --git a/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java b/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java
new file mode 100644
index 0000000..291edcd
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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
+ * 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.apache.sling.auth.core.impl;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.apache.sling.api.auth.Authenticator;
+import org.apache.sling.auth.core.spi.BundleAuthenticationRequirement;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ServiceScope;
+
+@Component(service = BundleAuthenticationRequirement.class,
+ scope = ServiceScope.BUNDLE,
+ property = Constants.SERVICE_VENDOR + "=The Apache Software Foundation")
+public class BundleAuthenticationRequirementImpl implements BundleAuthenticationRequirement {
+
+ static final String PREFIX = "Apache Sling Authentication Requirements for Bundle ";
+
+ /** The client bundle id. */
+ private long bundleId;
+
+ /** Provider string for info */
+ private String provider;
+
+ private PathBasedHolderCache<AuthenticationRequirementHolder> authRequiredCache;
+
+ @Reference
+ private Authenticator slingAuthenticator;
+
+ @Activate
+ private void activate(final ComponentContext componentCtx) {
+ final Bundle bundle = componentCtx.getUsingBundle();
+ this.bundleId = bundle.getBundleId();
+ this.provider = PREFIX +
+ bundle.getSymbolicName() +
+ ":" +
+ bundle.getVersion() +
+ " (" +
+ String.valueOf(bundle.getBundleId()) +
+ ")";
+ this.authRequiredCache = ((SlingAuthenticator)slingAuthenticator).authRequiredCache;
+ }
+
+ @Deactivate
+ private void deactivate() {
+ clearRequirements();
+ }
+
+ @Override
+ public void setRequirements(@Nonnull final Map<String, Boolean> requirements) {
+ final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+
+ // remove existing entries
+ clearRequirements();
+
+ // register the new entries
+ register(reqHolders);
+ }
+
+ @Override
+ public void appendRequirements(@Nonnull final Map<String, Boolean> requirements) {
+ final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+ register(reqHolders);
+ }
+
+ @Override
+ public void removeRequirements(@Nonnull final Map<String, Boolean> requirements) {
+ final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+ for (AuthenticationRequirementHolder authReq : reqHolders) {
+ authRequiredCache.removeHolder(authReq);
+ }
+ }
+
+ @Override
+ public void clearRequirements() {
+ authRequiredCache.removeAllMatchingHolders(new AuthenticationRequirementHolder("", false, null) {
+
+ @Override
+ public boolean equals(final Object other) {
+ if ( other instanceof BundleAuthenticationRequirementHolder
+ && ((BundleAuthenticationRequirementHolder)other).getBundleId() == bundleId ) {
+ return true;
+ }
+ return false;
+ }
+ });
+
+ }
+
+ private Set<AuthenticationRequirementHolder> createHolders(@Nonnull Map<String,Boolean> requirements) {
+ final Set<AuthenticationRequirementHolder> holders = new HashSet<AuthenticationRequirementHolder>(requirements.size());
+ for (final Map.Entry<String, Boolean> entry : requirements.entrySet()) {
+ holders.add(new BundleAuthenticationRequirementHolder(entry.getKey(), entry.getValue()));
+ }
+ return holders;
+ }
+
+ private void register(@Nonnull final Collection<AuthenticationRequirementHolder> authReqs) {
+ for (AuthenticationRequirementHolder authReq : authReqs) {
+ authRequiredCache.addHolder(authReq);
+ }
+ }
+
+ private final class BundleAuthenticationRequirementHolder extends AuthenticationRequirementHolder {
+
+ public BundleAuthenticationRequirementHolder(final String fullPath,
+ final boolean requiresAuthentication) {
+ super(fullPath, requiresAuthentication, null);
+ }
+
+ @Override
+ String getProvider() {
+ return provider;
+ }
+
+ long getBundleId() {
+ return bundleId;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
index 97c59df..c634174 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
@@ -129,7 +129,7 @@
* is ordered the service description of the {@link SlingAuthenticator} is
* returned.
*/
- final String getProvider() {
+ String getProvider() {
// assume the commons/auth SlingAuthenticator provides the entry
if (serviceReference == null) {
return SlingAuthenticator.DESCRIPTION;
@@ -176,6 +176,9 @@
// now compare the service references giving priority to
// to the higher priority service
if (serviceReference == null) {
+ if ( other.serviceReference == null ) {
+ return 0;
+ }
return -1;
} else if (other.serviceReference == null) {
return 1;
diff --git a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
index 54267dc..28fbec5 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
@@ -21,6 +21,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
@@ -99,7 +100,35 @@
}
}
- public Collection<Type>[] findApplicableHolder(final HttpServletRequest request) {
+ /**
+ * Remove all holders which "equal" the provided holder
+ * @param holder Template holder
+ */
+ public void removeAllMatchingHolders(final Type holder) {
+ this.rwLock.writeLock().lock();
+ try {
+ for(final Map.Entry<String, Map<String, SortedSet<Type>>> entry : this.cache.entrySet()) {
+ final Iterator<Map.Entry<String, SortedSet<Type>>> innerIter = entry.getValue().entrySet().iterator();
+ while ( innerIter.hasNext() ) {
+ final Map.Entry<String, SortedSet<Type>> innerEntry = innerIter.next();
+ final Iterator<Type> iter = innerEntry.getValue().iterator();
+ while ( iter.hasNext() ) {
+ final Type current = iter.next();
+ if ( holder.equals(current) ) {
+ iter.remove();
+ }
+ }
+ if ( innerEntry.getValue().isEmpty() ) {
+ innerIter.remove();
+ }
+ }
+ }
+ } finally {
+ this.rwLock.writeLock().unlock();
+ }
+ }
+
+ public Collection<Type>[] findApplicableHolders(final HttpServletRequest request) {
this.rwLock.readLock().lock();
try {
final String hostname = request.getServerName()
diff --git a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
index 72efadf..60434b4 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
@@ -529,7 +529,7 @@
// select path used for authentication handler selection
final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache
- .findApplicableHolder(request);
+ .findApplicableHolders(request);
final String path = getHandlerSelectionPath(request);
boolean done = false;
for (int m = 0; !done && m < holdersArray.length; m++) {
@@ -592,7 +592,7 @@
final String path = getHandlerSelectionPath(request);
final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache
- .findApplicableHolder(request);
+ .findApplicableHolders(request);
for (int m = 0; m < holdersArray.length; m++) {
final Collection<AbstractAuthenticationHandlerHolder> holderSet = holdersArray[m];
if (holderSet != null) {
@@ -711,7 +711,7 @@
}
final Collection<AbstractAuthenticationHandlerHolder>[] localArray = this.authHandlerCache
- .findApplicableHolder(request);
+ .findApplicableHolders(request);
for (int m = 0; m < localArray.length; m++) {
final Collection<AbstractAuthenticationHandlerHolder> local = localArray[m];
if (local != null) {
@@ -905,7 +905,7 @@
}
final Collection<AuthenticationRequirementHolder>[] holderSetArray = authRequiredCache
- .findApplicableHolder(request);
+ .findApplicableHolders(request);
for (int m = 0; m < holderSetArray.length; m++) {
final Collection<AuthenticationRequirementHolder> holders = holderSetArray[m];
if (holders != null) {
diff --git a/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java b/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java
new file mode 100644
index 0000000..c3d5d07
--- /dev/null
+++ b/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) 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
+ * 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.apache.sling.auth.core.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.auth.core.spi.BundleAuthenticationRequirement;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Version;
+import org.osgi.service.component.ComponentContext;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+import junitx.util.PrivateAccessor;
+
+public class BundleAuthenticationRequirementImplTest {
+
+ private final SlingAuthenticator slingAuthenticator = new SlingAuthenticator();
+
+ private Predicate<AuthenticationRequirementHolder> providerFilter = new Predicate<AuthenticationRequirementHolder>() {
+ @Override
+ public boolean apply(AuthenticationRequirementHolder input) {
+ return input != null && input.getProvider().startsWith(BundleAuthenticationRequirementImpl.PREFIX);
+ }
+ };
+
+ Map<String, Boolean> initialRequirements = new HashMap<String, Boolean>();
+
+ BundleAuthenticationRequirement requirement;
+
+ @Before public void setUp() throws Throwable {
+ final Bundle bundle = mock(Bundle.class);
+ final ComponentContext cmpCtx = mock(ComponentContext.class);
+ when(cmpCtx.getUsingBundle()).thenReturn(bundle);
+ when(bundle.getBundleId()).thenReturn(5L);
+ when(bundle.getSymbolicName()).thenReturn("testbundle");
+ when(bundle.getVersion()).thenReturn(new Version("1.0.0"));
+
+ this.requirement = new BundleAuthenticationRequirementImpl();
+ PrivateAccessor.setField(this.requirement, "slingAuthenticator", slingAuthenticator);
+ PrivateAccessor.invoke(requirement, "activate", new Class[] {ComponentContext.class}, new Object[] {cmpCtx});
+
+ // add initial values for external auth requirements of the test service reference
+ initialRequirements.put("/a", true);
+ initialRequirements.put("/b", true);
+ initialRequirements.put("/b/c", false);
+ initialRequirements.put("/c", false);
+ requirement.setRequirements(initialRequirements);
+ }
+
+ private Iterable<AuthenticationRequirementHolder> filterRequirements() {
+ return Iterables.filter(slingAuthenticator.getAuthenticationRequirements(), providerFilter);
+ }
+
+ private void assertRequirements(Map<String, Boolean> expected, Iterable<AuthenticationRequirementHolder> requirements) {
+ assertEquals(expected.size(), Iterables.size(requirements));
+
+ for (AuthenticationRequirementHolder holder : requirements) {
+ assertTrue(holder.fullPath, expected.containsKey(holder.fullPath));
+ boolean b = expected.get(holder.fullPath);
+ assertEquals(holder.fullPath, b, holder.requiresAuthentication());
+ }
+ }
+
+ @Test public void test_VerifyInitialSetup() throws Exception {
+ assertRequirements(initialRequirements, filterRequirements());
+ }
+
+ @Test public void test_SetRequirements() throws Exception {
+ Map<String, Boolean> toReplace = ImmutableMap.of("/a", false, "/d", true);
+ requirement.setRequirements(toReplace);
+
+ // it's expected that all existing values have been replaced
+ assertRequirements(toReplace, filterRequirements());
+ }
+
+ @Test public void test_AppendRequirements() throws Exception {
+ Map<String, Boolean> toAppend = ImmutableMap.of("/d", true, "/e/f", false);
+ requirement.appendRequirements(toAppend);
+
+ // the expected result is the combination of the entries to append plus
+ // the initial values.
+ Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+ expected.putAll(toAppend);
+ assertRequirements(expected, filterRequirements());
+ }
+
+ @Test public void test_AppendRequirementsWithConflict() throws Exception {
+ Map<String, Boolean> toAppend = ImmutableMap.of("/a", false, "/d", true, "/e/f", false);
+ requirement.appendRequirements(toAppend);
+
+ // the expected result is the combination of the entries to append plus
+ // the initial values; conflicting values (same path again) wont'be replaced
+ // and the 'conflicting' entries are ignored.
+ Map<String, Boolean> expected = new HashMap<String, Boolean>(toAppend);
+ expected.putAll(initialRequirements);
+ assertRequirements(expected, filterRequirements());
+ }
+
+ @Test public void test_RemoveRequirements() throws Exception {
+ Map<String, Boolean> toRemove = ImmutableMap.of("/a", true, "/b", true);
+ requirement.removeRequirements(toRemove);
+
+ // the expected result is initial set without the entries to be removed.
+ Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+ for (String key : toRemove.keySet()) {
+ expected.remove(key);
+ }
+ assertRequirements(expected, filterRequirements());
+ }
+
+ @Test public void test_RemoveRequirements2() throws Exception {
+ // paths to remove are contained but have a different value
+ Map<String, Boolean> toRemove = ImmutableMap.of("/a", false, "/b", false);
+ requirement.removeRequirements(toRemove);
+
+ // the expected result is the initial set without the entries to be removed.
+ Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+ for (String key : toRemove.keySet()) {
+ expected.remove(key);
+ }
+ assertRequirements(expected, filterRequirements());
+ }
+
+ @Test public void test_RemoveNonExistingRequirements() throws Exception {
+ // paths to remove are contained but have a different value
+ Map<String, Boolean> toRemove = ImmutableMap.of("/nonExisting", true);
+ requirement.removeRequirements(toRemove);
+
+ // the expected result is the initial set
+ assertRequirements(initialRequirements, filterRequirements());
+ }
+
+ @Test public void test_ClearRequirements() throws Exception {
+ requirement.clearRequirements();
+
+ assertRequirements(ImmutableMap.<String, Boolean>of(), filterRequirements());
+
+ }
+}
\ No newline at end of file