/* * #%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.httpcache.config.impl; import com.adobe.acs.commons.httpcache.config.HttpCacheConfig; import com.adobe.acs.commons.httpcache.config.HttpCacheConfigExtension; import com.adobe.acs.commons.httpcache.exception.HttpCacheKeyCreationException; import com.adobe.acs.commons.httpcache.exception.HttpCacheRepositoryAccessException; import com.adobe.acs.commons.httpcache.keys.AbstractCacheKey; import com.adobe.acs.commons.httpcache.keys.CacheKey; import com.adobe.acs.commons.httpcache.keys.CacheKeyFactory; import com.adobe.acs.commons.httpcache.util.UserUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.felix.scr.annotations.Activate; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.ConfigurationPolicy; import org.apache.felix.scr.annotations.Modified; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.PropertyUnbounded; import org.apache.felix.scr.annotations.Service; import org.apache.jackrabbit.api.security.user.User; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.commons.osgi.PropertiesUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import java.util.Map; /** * Implementation for custom cache config extension and associated cache key creation based on aem groups. This cache * config extension accepts the http request only if at least one of the configured groups is present in the request * user's group membership list. Made it as config factory as it could move along 1-1 with HttpCacheConfig. */ @Component(label = "ACS AEM Commons - HTTP Cache - Group based extension for HttpCacheConfig and CacheKeyFactory.", description = "HttpCacheConfig custom extension for group based configuration and associated cache key " + "creation.", metatype = true, configurationFactory = true, policy = ConfigurationPolicy.REQUIRE ) @Properties({ @Property(name = "webconsole.configurationFactory.nameHint", value = "Allowed user groups: {httpcache.config.extension.user-groups.allowed}", propertyPrivate = true) }) @Service public class GroupHttpCacheConfigExtension implements HttpCacheConfigExtension, CacheKeyFactory { private static final Logger log = LoggerFactory.getLogger(GroupHttpCacheConfigExtension.class); // Custom cache config attributes @Property(label = "Allowed user groups", description = "Users groups that are used to accept and create cache keys.", unbounded = PropertyUnbounded.ARRAY) private static final String PROP_USER_GROUPS = "httpcache.config.extension.user-groups.allowed"; private List<String> userGroups; //-------------------------<HttpCacheConfigExtension methods> @Override public boolean accepts(SlingHttpServletRequest request, HttpCacheConfig cacheConfig) throws HttpCacheRepositoryAccessException { // Match groups. if (UserUtils.isAnonymous(request.getResourceResolver().getUserID())) { // If the user is anonymous, no matching with groups required. return true; } else { // Case of authenticated requests. if (userGroups.isEmpty()) { // In case custom attributes list is empty. if (log.isTraceEnabled()) { log.trace("GroupHttpCacheConfigExtension accepts request [ {} ]", request.getRequestURI()); } return true; } try { List<String> requestUserGroupNames = UserUtils.getUserGroupMembershipNames(request .getResourceResolver().adaptTo(User.class)); // At least one of the group in config should match. boolean isGroupMatchFound = CollectionUtils.containsAny(userGroups, requestUserGroupNames); if (!isGroupMatchFound) { log.trace("Group didn't match and hence rejecting the cache config."); } else { if (log.isTraceEnabled()) { log.trace("GroupHttpCacheConfigExtension accepts request [ {} ]", request.getRequestURI()); } } return isGroupMatchFound; } catch (RepositoryException e) { throw new HttpCacheRepositoryAccessException("Unable to access group information of request user.", e); } } } //-------------------------<CacheKeyFactory methods> @Override public CacheKey build(final SlingHttpServletRequest slingHttpServletRequest, final HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException { return new GroupCacheKey(slingHttpServletRequest, cacheConfig); } @Override public CacheKey build(final String resourcePath, final HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException { return new GroupCacheKey(resourcePath, cacheConfig); } @Override public boolean doesKeyMatchConfig(CacheKey key, HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException { // Check if key is instance of GroupCacheKey. if (!(key instanceof GroupCacheKey)) { return false; } // Validate if key request uri can be constructed out of uri patterns in cache config. return new GroupCacheKey(key.getUri(), cacheConfig).equals(key); } /** * The GroupCacheKey is a custom CacheKey bound to this particular factory. */ class GroupCacheKey extends AbstractCacheKey implements CacheKey { /* This key is composed of uri, list of user groups and authentication requirement details */ private List<String> cacheKeyUserGroups; public GroupCacheKey(SlingHttpServletRequest request, HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException { super(request, cacheConfig); this.cacheKeyUserGroups = userGroups; } public GroupCacheKey(String uri, HttpCacheConfig cacheConfig) throws HttpCacheKeyCreationException { super(uri, cacheConfig); this.cacheKeyUserGroups = userGroups; } @Override public boolean equals(Object o) { if (!super.equals(o)) { return false; } GroupCacheKey that = (GroupCacheKey) o; return new EqualsBuilder() .append(getUri(), that.getUri()) .append(cacheKeyUserGroups, that.cacheKeyUserGroups) .append(getAuthenticationRequirement(), that.getAuthenticationRequirement()) .isEquals(); } @Override public int hashCode() { return new HashCodeBuilder(17, 37) .append(getUri()) .append(cacheKeyUserGroups) .append(getAuthenticationRequirement()).toHashCode(); } @Override public String toString() { StringBuilder formattedString = new StringBuilder(this.uri).append(" [GROUPS:"); formattedString.append(StringUtils.join(cacheKeyUserGroups, "|")); formattedString.append("] [AUTH_REQ:" + getAuthenticationRequirement() + "]"); return formattedString.toString(); } } //-------------------------<OSGi Component methods> @Activate @Modified protected void activate(Map<String, Object> configs) { // User groups after removing empty strings. userGroups = new ArrayList(Arrays.asList(PropertiesUtil.toStringArray(configs.get(PROP_USER_GROUPS), new String[]{}))); ListIterator<String> listIterator = userGroups.listIterator(); while (listIterator.hasNext()) { String value = listIterator.next(); if (StringUtils.isBlank(value)) { listIterator.remove(); } } log.info("GroupHttpCacheConfigExtension activated/modified."); } }