blob: 43a5a0d9be2545be8793d6453bc2044676d1364f [file] [log] [blame]
/*******************************************************************************
* Copyright 2017 General Electric Company
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*******************************************************************************/
package com.ge.predix.acs.policy.evaluation.cache;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.codehaus.jackson.map.ObjectMapper;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Minutes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ge.predix.acs.attribute.connector.management.AttributeConnectorService;
import com.ge.predix.acs.privilege.management.dao.ResourceEntity;
import com.ge.predix.acs.privilege.management.dao.SubjectEntity;
import com.ge.predix.acs.rest.PolicyEvaluationResult;
enum EntityType {
RESOURCE("resource"),
SUBJECT("subject"),
POLICY_SET("policy set");
private final String name;
EntityType(final String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
@Component
public abstract class AbstractPolicyEvaluationCache implements PolicyEvaluationCache {
@Autowired
private AttributeConnectorService connectorService;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractPolicyEvaluationCache.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* This method will get the Policy Evaluation Result from the cache. It will check if any of the subject, policy
* sets or resolved resource URI's have a timestamp in the cache after the timestamp of the Policy Evaluation
* Result. If the key is not in the cache or the result is invalidated, it will return null. Also it will remove
* the Policy EvaluationResult so that subsequent evaluations won't find the key in the cache.
*
* @param evalRequestkey The Policy Evaluation key to retrieve.
* @return The Policy Evaluation Result if the key is in the cache and the result isn't invalidated, or null
*/
@Override
public PolicyEvaluationResult get(final PolicyEvaluationRequestCacheKey evalRequestkey) {
//Get all result related entries
DecisionCacheEntries cachedEntries = new DecisionCacheEntries(evalRequestkey);
String cachedEvalResultString = cachedEntries.getDecisionString();
if (null == cachedEvalResultString) {
return null;
}
PolicyEvaluationResult cachedEvalResult = toPolicyEvaluationResult(cachedEvalResultString);
List<String> attributeInvalidationTimeStamps = new ArrayList<>();
List<String> policyInvalidationTimeStamps = new ArrayList<>();
attributeInvalidationTimeStamps.add(cachedEntries.getSubjectLastModified());
policyInvalidationTimeStamps.addAll(cachedEntries.getPolicySetsLastModified());
Set<String> cachedResolvedResourceUris = cachedEvalResult.getResolvedResourceUris();
//is requested resource id same as resolved resource uri ?
if (cachedResolvedResourceUris.size() == 1 && cachedResolvedResourceUris.iterator().next()
.equals(evalRequestkey.getResourceId())) {
attributeInvalidationTimeStamps.add(cachedEntries.getRequestedResourceLastModified());
} else {
List<String> cacheResolvedResourceKeys = cachedResolvedResourceUris.stream()
.map(resolvedResourceUri -> resourceKey(evalRequestkey.getZoneId(), resolvedResourceUri))
.collect(Collectors.toList());
attributeInvalidationTimeStamps.addAll(multiGet(cacheResolvedResourceKeys));
}
if (isCachedRequestInvalid(attributeInvalidationTimeStamps, policyInvalidationTimeStamps,
timestampToDateUTC(cachedEvalResult.getTimestamp()))) {
delete(cachedEntries.getDecisionKey());
LOGGER.debug("Cached decision for key '{}' is not valid.", cachedEntries.getDecisionKey());
return null;
}
return cachedEvalResult;
}
private PolicyEvaluationResult toPolicyEvaluationResult(final String cachedDecisionString) {
try {
return OBJECT_MAPPER.readValue(cachedDecisionString, PolicyEvaluationResult.class);
} catch (IOException e) {
throw new IllegalStateException("Failed to read policy evaluation result as JSON.", e);
}
}
private final class DecisionCacheEntries {
private final List<String> entryValues;
private final List<String> entryKeys;
private final List<String> policySetTimestamps = new ArrayList<>();
private final int lastValueIndex;
private final String decisionKey;
/**
* Execute a multi-get operation on all entries related to a cached result,
* and provides a immutable object for the values fetched.
*/
DecisionCacheEntries(final PolicyEvaluationRequestCacheKey evalRequestKey) {
//Get all values with a batch get
this.decisionKey = evalRequestKey.toDecisionKey();
this.entryKeys = prepareKeys(evalRequestKey);
this.entryValues = multiGet(this.entryKeys);
this.lastValueIndex = this.entryValues.size() - 1;
logCacheGetDebugMessages(evalRequestKey, this.decisionKey, this.entryKeys, this.entryValues);
//create separate list of policySetTimes to prevent mutation on entryValues
for (int i = 0; i < evalRequestKey.getPolicySetIds().size(); i++) {
this.policySetTimestamps.add(this.entryValues.get(i));
}
}
//Prepare keys to fetch in one batch
private List<String> prepareKeys(final PolicyEvaluationRequestCacheKey evalRequestKey) {
List<String> keys = new ArrayList<>();
//Add 'n' Policy Set keys
LinkedHashSet<String> policySetIds = evalRequestKey.getPolicySetIds();
policySetIds.forEach(policySetId -> keys.add(policySetKey(evalRequestKey.getZoneId(), policySetId)));
// n+1
keys.add(subjectKey(evalRequestKey.getZoneId(), evalRequestKey.getSubjectId()));
//n+2
keys.add(resourceKey(evalRequestKey.getZoneId(), evalRequestKey.getResourceId()));
//n+3
keys.add(this.decisionKey);
return keys;
}
/**
* (eval result, eval time, resolved resource uri(s)).
*/
String getDecisionString() {
return entryValues.get(this.lastValueIndex);
}
String getDecisionKey() {
return decisionKey;
}
String getSubjectLastModified() {
return entryValues.get(lastValueIndex - 2);
}
String getRequestedResourceLastModified() {
return entryValues.get(lastValueIndex - 1);
}
List<String> getPolicySetsLastModified() {
return this.policySetTimestamps;
}
}
private void logCacheGetDebugMessages(final PolicyEvaluationRequestCacheKey key, final String redisKey,
final List<String> keys, final List<String> values) {
LinkedHashSet<String> policySetIds = key.getPolicySetIds();
int idx = 0;
for (String policySetId : policySetIds) {
LOGGER.debug("Getting timestamp for policy set: '{}', key: '{}', timestamp:'{}'.", policySetId,
keys.get(idx), values.get(idx++));
}
LOGGER.debug("Getting timestamp for subject: '{}', key: '{}', timestamp:'{}'.", key.getSubjectId(),
keys.get(idx), values.get(idx++));
LOGGER.debug("Getting timestamp for resource: '{}', key: '{}', timestamp:'{}'.", key.getResourceId(),
keys.get(idx), values.get(idx++));
LOGGER.debug("Getting policy evaluation from cache; key: '{}', value: '{}'.", redisKey, values.get(idx));
}
// Set's the policy evaluation key to the policy evaluation result in the cache
@Override
public void set(final PolicyEvaluationRequestCacheKey key, final PolicyEvaluationResult result) {
try {
setEntityTimestamps(key, result);
result.setTimestamp(currentDateUTC().getMillis());
String value = OBJECT_MAPPER.writeValueAsString(result);
set(key.toDecisionKey(), value);
LOGGER.debug("Setting policy evaluation to cache; key: '{}', value: '{}'.", key.toDecisionKey(), value);
} catch (IOException e) {
throw new IllegalArgumentException("Failed to write policy evaluation result as JSON.", e);
}
}
private void setEntityTimestamps(final PolicyEvaluationRequestCacheKey key, final PolicyEvaluationResult result) {
// This ensures that if the timestamp for any entity involved in this decision is not in the cache at the time
// of this evaluation, it will be put there so that in subsequent evaluations, we will use the cached
// decision.
// We reset the timestamp to now for entities only if they do not exist in the cache so that we don't
// invalidate previous cached decisions.
LOGGER.debug("Setting timestamp to now for entities if they do not exist in the cache");
String zoneId = key.getZoneId();
setSubjectIfNotExists(zoneId, key.getSubjectId());
Set<String> resolvedResourceUris = result.getResolvedResourceUris();
for (String resolvedResourceUri : resolvedResourceUris) {
setResourceIfNotExists(zoneId, resolvedResourceUri);
}
Set<String> policySetIds = key.getPolicySetIds();
for (String policySetId : policySetIds) {
setPolicySetIfNotExists(zoneId, policySetId);
}
}
@Override
public void reset() {
flushAll();
}
@Override
public void reset(final PolicyEvaluationRequestCacheKey key) {
Set<String> keys = keys(key.toDecisionKey());
delete(keys);
}
// Method which resets the timestamp for the given entity in the policy evaluation cache.
private void resetForEntity(final String zoneId, final String entityId, final EntityType entityType,
final BiFunction<String, String, String> getKey) {
String key = getKey.apply(zoneId, entityId);
String timestamp = timestampUTC();
logSetEntityTimestampsDebugMessage(timestamp, key, entityId, entityType);
set(key, timestamp);
}
private void setEntityIfNotExists(final String zoneId, final String entityId,
final BiFunction<String, String, String> getKey) {
String key = getKey.apply(zoneId, entityId);
setIfNotExists(key, timestampUTC());
}
private void logSetEntityTimestampsDebugMessage(final String timestamp, final String key, final String entityId,
final EntityType entityType) {
LOGGER.debug("Setting timestamp for {} '{}'; key: '{}', value: '{}'", entityType, entityId, key, timestamp);
}
@Override
public void resetForPolicySet(final String zoneId, final String policySetId) {
resetForEntity(zoneId, policySetId, EntityType.POLICY_SET, AbstractPolicyEvaluationCache::policySetKey);
}
private void setPolicySetIfNotExists(final String zoneId, final String policySetId) {
setEntityIfNotExists(zoneId, policySetId, AbstractPolicyEvaluationCache::policySetKey);
}
@Override
public void resetForResource(final String zoneId, final String resourceId) {
resetForEntity(zoneId, resourceId, EntityType.RESOURCE, AbstractPolicyEvaluationCache::resourceKey);
}
private void setResourceIfNotExists(final String zoneId, final String resourceId) {
setEntityIfNotExists(zoneId, resourceId, AbstractPolicyEvaluationCache::resourceKey);
}
@Override
public void resetForResourcesByIds(final String zoneId, final Set<String> resourceIds) {
Map<String, String> map = new HashMap<>();
for (String resourceId : resourceIds) {
createMutliSetEntityMap(zoneId, map, resourceId, EntityType.RESOURCE,
AbstractPolicyEvaluationCache::resourceKey);
}
multiSet(map);
}
@Override
public void resetForResources(final String zoneId, final List<ResourceEntity> resourceEntities) {
Map<String, String> map = new HashMap<>();
for (ResourceEntity resourceEntity : resourceEntities) {
createMutliSetEntityMap(zoneId, map, resourceEntity.getResourceIdentifier(), EntityType.RESOURCE,
AbstractPolicyEvaluationCache::resourceKey);
}
multiSet(map);
}
@Override
public void resetForSubject(final String zoneId, final String subjectId) {
resetForEntity(zoneId, subjectId, EntityType.SUBJECT, AbstractPolicyEvaluationCache::subjectKey);
}
private void setSubjectIfNotExists(final String zoneId, final String subjectId) {
setEntityIfNotExists(zoneId, subjectId, AbstractPolicyEvaluationCache::subjectKey);
}
@Override
public void resetForSubjectsByIds(final String zoneId, final Set<String> subjectIds) {
Map<String, String> map = new HashMap<>();
for (String subjectId : subjectIds) {
createMutliSetEntityMap(zoneId, map, subjectId, EntityType.SUBJECT,
AbstractPolicyEvaluationCache::subjectKey);
}
multiSet(map);
}
@Override
public void resetForSubjects(final String zoneId, final List<SubjectEntity> subjectEntities) {
Map<String, String> map = new HashMap<>();
for (SubjectEntity subjectEntity : subjectEntities) {
createMutliSetEntityMap(zoneId, map, subjectEntity.getSubjectIdentifier(), EntityType.SUBJECT,
AbstractPolicyEvaluationCache::subjectKey);
}
multiSet(map);
}
private void createMutliSetEntityMap(final String zoneId, final Map<String, String> map, final String subjectId,
final EntityType entityType, final BiFunction<String, String, String> getKey) {
String key = getKey.apply(zoneId, subjectId);
String timestamp = timestampUTC();
logSetEntityTimestampsDebugMessage(key, timestamp, subjectId, entityType);
map.put(key, timestamp);
}
private boolean isCachedRequestInvalid(final List<String> attributeInvalidationTimeStamps,
final List<String> policyInvalidationTimeStamps, final DateTime policyEvalTimestampUTC) {
if (haveEntitiesChanged(policyInvalidationTimeStamps, policyEvalTimestampUTC)) {
return true;
}
if (this.connectorService.isResourceAttributeConnectorConfigured() || this.connectorService
.isSubjectAttributeConnectorConfigured()) {
return haveConnectorCacheIntervalsLapsed(this.connectorService, policyEvalTimestampUTC);
} else {
return haveEntitiesChanged(attributeInvalidationTimeStamps, policyEvalTimestampUTC);
}
}
/**
* This method checks to see if any objects related to the policy evaluation have been changed since the Policy
* Evaluation Result was cached.
*
* @param values List of values which contain subject, policy sets and resolved resource URI's.
* @param policyEvalTimestampUTC The timestamp to compare against.
* @return true or false depending on whether any of the objects in values has a timestamp after
* policyEvalTimestampUTC.
*/
boolean haveEntitiesChanged(final List<String> values, final DateTime policyEvalTimestampUTC) {
for (String value : values) {
if (null == value) {
return true;
}
DateTime invalidationTimestampUTC = timestampToDateUTC(value);
if (invalidationTimestampUTC.isAfter(policyEvalTimestampUTC)) {
LOGGER.debug("Privilege service attributes have timestamp '{}' which is after "
+ "policy evaluation timestamp '{}'", invalidationTimestampUTC, policyEvalTimestampUTC);
return true;
}
}
return false;
}
boolean haveConnectorCacheIntervalsLapsed(final AttributeConnectorService localConnectorService,
final DateTime policyEvalTimestampUTC) {
DateTime nowUTC = currentDateUTC();
int decisionAgeMinutes = Minutes.minutesBetween(policyEvalTimestampUTC, nowUTC).getMinutes();
boolean hasResourceConnectorIntervalLapsed = localConnectorService.isResourceAttributeConnectorConfigured()
&& decisionAgeMinutes >= localConnectorService.getResourceAttributeConnector()
.getMaxCachedIntervalMinutes();
boolean hasSubjectConnectorIntervalLapsed = localConnectorService.isSubjectAttributeConnectorConfigured()
&& decisionAgeMinutes >= localConnectorService.getSubjectAttributeConnector()
.getMaxCachedIntervalMinutes();
return hasResourceConnectorIntervalLapsed || hasSubjectConnectorIntervalLapsed;
}
static String policySetKey(final String zoneId, final String policySetId) {
return zoneId + ":set-id:" + Integer.toHexString(policySetId.hashCode());
}
static String resourceKey(final String zoneId, final String resourceId) {
return zoneId + ":res-id:" + Integer.toHexString(resourceId.hashCode());
}
static String subjectKey(final String zoneId, final String subjectId) {
return zoneId + ":sub-id:" + Integer.toHexString(subjectId.hashCode());
}
static boolean isPolicyEvalResultKey(final String key) {
return key.matches("^[^:]*:[^:]*:[^:]*:[^:]*$");
}
static boolean isPolicySetChangedKey(final String key) {
return key.matches("^[^:]*:set-id:[^:]*$");
}
static boolean isResourceChangedKey(final String key) {
return key.matches("^[^:]*:res-id:[^:]*$");
}
static boolean isSubjectChangedKey(final String key) {
return key.matches("^[^:]*:sub-id:[^:]*$");
}
private static DateTime currentDateUTC() {
return new DateTime().withZone(DateTimeZone.UTC);
}
private static String timestampUTC() {
try {
return OBJECT_MAPPER.writeValueAsString(currentDateUTC().getMillis());
} catch (IOException e) {
throw new IllegalStateException("Failed to write timestamp as JSON.", e);
}
}
private static DateTime timestampToDateUTC(final long timestamp) {
return new DateTime(timestamp).withZone(DateTimeZone.UTC);
}
private static DateTime timestampToDateUTC(final String timestamp) {
return new DateTime(Long.valueOf(timestamp)).withZone(DateTimeZone.UTC);
}
abstract void delete(String key);
abstract void delete(Collection<String> keys);
abstract void flushAll();
abstract Set<String> keys(String key);
abstract List<String> multiGet(List<String> keys);
abstract void multiSet(Map<String, String> map);
abstract void set(String key, String value);
abstract void setIfNotExists(String key, String value);
}