Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple filter which prevents access to URLs based on allowed selectors, ... #438

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions bundle/src/main/java/com/adobe/acs/commons/util/impl/UrlFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2015 Adobe
* %%
* 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.
* #L%
*/
package com.adobe.acs.commons.util.impl;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.apache.commons.lang.ArrayUtils;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.sling.SlingFilter;
import org.apache.felix.scr.annotations.sling.SlingFilterScope;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.wcm.api.components.Component;
import com.day.cq.wcm.api.components.ComponentManager;

/**
* Filter which can accept/reject requests based on the selectors, extensions, and/or suffixes.
* Allowed selector, extensions, and suffix patterns are defined on the component resource using the property names
* defined in the constants in this class.
*/
@org.apache.felix.scr.annotations.Component(policy = ConfigurationPolicy.REQUIRE)
@SlingFilter(scope = SlingFilterScope.REQUEST, order = Integer.MIN_VALUE, generateComponent = false)
public class UrlFilter implements Filter {

/**
* Regular expression for the allowed extensions. Property with this name must be single-valued.
*/
protected static final String PN_ALLOWED_EXTENSION_PATTERN = "allowedExtensionPattern";

/**
* List of values which will be allowed extensions.
* If this property is an empty array, no extensions will be allowed.
*/
protected static final String PN_ALLOWED_EXTENSIONS = "allowedExtensions";

/**
* Regular expression for the allowed selectors. Property with this name must be single-valued.
*/
protected static final String PN_ALLOWED_SELECTOR_PATTERN = "allowedSelectorPattern";

/**
* List of values which will be allowed selectors.
* If this property is an empty array, no selectors will be allowed.
*/
protected static final String PN_ALLOWED_SELECTORS = "allowedSelectors";

/**
* Regular expression for the allowed suffixes. Property with this name must be single-valued.
*/
protected static final String PN_ALLOWED_SUFFIX_PATTERN = "allowedSuffixPattern";

/**
* List of values which will be allowed suffixes.
* If this property is an empty array, no suffixes will be allowed.
*/
protected static final String PN_ALLOWED_SUFFIXES = "allowedSuffixes";

private static final Collection<String> PROPERTY_NAMES = Arrays.asList(PN_ALLOWED_SUFFIXES, PN_ALLOWED_EXTENSIONS,
PN_ALLOWED_SELECTORS, PN_ALLOWED_SUFFIX_PATTERN, PN_ALLOWED_SELECTOR_PATTERN, PN_ALLOWED_EXTENSION_PATTERN);

private static final Logger log = LoggerFactory.getLogger(UrlFilter.class);

public void destroy() {
// nothing to do
}

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (request instanceof SlingHttpServletRequest && response instanceof SlingHttpServletResponse) {
SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;

RequestPathInfo pathInfo = slingRequest.getRequestPathInfo();

Component definitionComponent = findUrlFilterDefinitionComponent(slingRequest.getResource(),
slingRequest.getResourceResolver().adaptTo(ComponentManager.class));

if (definitionComponent != null) {
String definitionPath = definitionComponent.getPath();
log.debug("found url filter definition resource at {}", definitionPath);
ValueMap properties = definitionComponent.getProperties();
if (properties != null) {
if (checkSelector(pathInfo, properties) && checkSuffix(pathInfo, properties)
&& checkExtension(pathInfo, properties)) {
log.debug("url filter definition resource at {} passed for request {}.",
definitionPath, slingRequest.getRequestPathInfo());
} else {
log.info("url filter definition resource at {} FAILED for request {}.",
definitionPath, slingRequest.getRequestPathInfo());
slingResponse.sendError(403);
return;
}
}
}

}

chain.doFilter(request, response);

}

public void init(FilterConfig filterConfig) throws ServletException {
// nothing to do
}

protected boolean checkExtension(RequestPathInfo pathInfo, ValueMap properties) {
return check(pathInfo.getExtension(), PN_ALLOWED_EXTENSIONS, PN_ALLOWED_EXTENSION_PATTERN, properties);
}

protected boolean checkSelector(RequestPathInfo pathInfo, ValueMap properties) {
return check(pathInfo.getSelectorString(), PN_ALLOWED_SELECTORS, PN_ALLOWED_SELECTOR_PATTERN, properties);
}

private boolean check(String value, String allowedArrayPropertyName, String allowedPatternPropertyName, ValueMap properties) {
if (value == null) {
// no value is always allowed
return true;
}
String[] allowedValues = properties.get(allowedArrayPropertyName, String[].class);
if (allowedValues != null) {
if (allowedValues.length == 0) {
log.debug("{} was empty, therefore not allowing any value.", allowedArrayPropertyName);
return false;
} else if (!ArrayUtils.contains(allowedValues, value)) {
log.debug("{} did not contain our string {}. checking the pattern.", allowedArrayPropertyName, value);
String allowedPattern = properties.get(allowedPatternPropertyName, String.class);
if (allowedPattern == null || !Pattern.matches(allowedPattern, value)) {
log.debug("allowedPattern ({}) did not match our string {}", allowedPattern, value);
return false;
} else {
log.debug("allowedPattern ({}) did match our string {}", allowedPattern, value);
return true;
}
} else {
return true;
}
} else {
String allowedPattern = properties.get(allowedPatternPropertyName, String.class);
if (allowedPattern != null && !Pattern.matches(allowedPattern, value)) {
log.debug("allowedPattern ({}) did not match our string {}", allowedPattern, value);
return false;
} else {
return true;
}
}
}

protected boolean checkSuffix(RequestPathInfo pathInfo, ValueMap properties) {
return check(pathInfo.getSuffix(), PN_ALLOWED_SUFFIXES, PN_ALLOWED_SUFFIX_PATTERN, properties);
}

private Component findUrlFilterDefinitionComponent(Resource resource, ComponentManager componentManager) {
if (resource == null) {
return null;
}

Resource contentResource = resource.getChild("jcr:content");
if (contentResource != null) {
resource = contentResource;
}

Component component = componentManager.getComponentOfResource(resource);

return findUrlFilterDefinitionComponent(component);
}

private Component findUrlFilterDefinitionComponent(Component component) {
if (component == null) {
return null;
}

ValueMap properties = component.getProperties();
// Collections.disjoint returns true if the collections
// have nothing in common, so when it is false, use the current resource
if (!Collections.disjoint(properties.keySet(), PROPERTY_NAMES)) {
return component;
} else {
// otherwise, look at the resource type resource's super type
return findUrlFilterDefinitionComponent(component.getSuperComponent());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* #%L
* ACS AEM Commons Bundle
* %%
* Copyright (C) 2015 Adobe
* %%
* 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.
* #L%
*/
package com.adobe.acs.commons.util.impl;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import java.util.HashMap;

import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class UrlFilterTest {
private ValueMap properties;

@Before
public void setup() {
properties = new ValueMapDecorator(new HashMap<String, Object>());
}

@Test
public void null_selector() {
UrlFilter filter = new UrlFilter();

RequestPathInfo testInfo = mock(RequestPathInfo.class);
when(testInfo.getSelectorString()).thenReturn(null);

assertTrue(filter.checkSelector(testInfo, null));
}

@Test
public void non_null_selector() {
UrlFilter filter = new UrlFilter();

RequestPathInfo testInfo = mock(RequestPathInfo.class);
when(testInfo.getSelectorString()).thenReturn("sample");

// null allowedSelectors = ok
assertTrue(filter.checkSelector(testInfo, properties));

// empty array allowedSelectors = fail
properties.put(UrlFilter.PN_ALLOWED_SELECTORS, (Object) new String[0]);
assertFalse(filter.checkSelector(testInfo, properties));

// selector string in array = ok
properties.put(UrlFilter.PN_ALLOWED_SELECTORS, (Object) new String[] { "sample", "sample2" });
assertTrue(filter.checkSelector(testInfo, properties));

// selector string not in array = fail
properties.put(UrlFilter.PN_ALLOWED_SELECTORS, (Object) new String[] { "other" });
assertFalse(filter.checkSelector(testInfo, properties));

properties.clear();

// matches regex
properties.put(UrlFilter.PN_ALLOWED_SELECTOR_PATTERN, "^s[a-z]m.*$");
assertTrue(filter.checkSelector(testInfo, properties));

// doesn't match regex
properties.put(UrlFilter.PN_ALLOWED_SELECTOR_PATTERN, "^s[1-2]m$");
assertFalse(filter.checkSelector(testInfo, properties));

properties.clear();

// matches array or regex = ok
properties.put(UrlFilter.PN_ALLOWED_SELECTORS, (Object) new String[] { "other" });
properties.put(UrlFilter.PN_ALLOWED_SELECTOR_PATTERN, "^s[a-z]m.*$");
assertTrue(filter.checkSelector(testInfo, properties));

properties.put(UrlFilter.PN_ALLOWED_SELECTORS, (Object) new String[] { "sample" });
properties.put(UrlFilter.PN_ALLOWED_SELECTOR_PATTERN, "^s[a-z]m$");
assertTrue(filter.checkSelector(testInfo, properties));

}
}