diff --git a/src/main/java/run/halo/app/cache/CacheStore.java b/src/main/java/run/halo/app/cache/CacheStore.java index 0e65cf9841bf0c653afe9b10a18b6f07a74f244c..8f00901f6fc4ab2931633e32f0f13c522130474d 100644 --- a/src/main/java/run/halo/app/cache/CacheStore.java +++ b/src/main/java/run/halo/app/cache/CacheStore.java @@ -1,5 +1,6 @@ package run.halo.app.cache; +import java.util.LinkedHashMap; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.springframework.lang.NonNull; @@ -61,4 +62,9 @@ public interface CacheStore { */ void delete(@NonNull K key); + /** + * Returns a view of the entries stored in this cache as a none thread-safe map. + * Modifications made to the map do not directly affect the cache. + */ + LinkedHashMap toMap(); } diff --git a/src/main/java/run/halo/app/cache/InMemoryCacheStore.java b/src/main/java/run/halo/app/cache/InMemoryCacheStore.java index 2b5bcad7615a32619f0de840abd6c4d50863b26d..69dd5c3bd5fef4c217401a3c2947a6de23bfc937 100644 --- a/src/main/java/run/halo/app/cache/InMemoryCacheStore.java +++ b/src/main/java/run/halo/app/cache/InMemoryCacheStore.java @@ -1,5 +1,6 @@ package run.halo.app.cache; +import java.util.LinkedHashMap; import java.util.Optional; import java.util.Timer; import java.util.TimerTask; @@ -58,7 +59,6 @@ public class InMemoryCacheStore extends AbstractStringCacheStore { // Put the cache wrapper CacheWrapper putCacheWrapper = CACHE_CONTAINER.put(key, cacheWrapper); - log.debug("Put [{}] cache result: [{}], original cache wrapper: [{}]", key, putCacheWrapper, cacheWrapper); } @@ -98,6 +98,13 @@ public class InMemoryCacheStore extends AbstractStringCacheStore { log.debug("Removed key: [{}]", key); } + @Override + public LinkedHashMap toMap() { + LinkedHashMap map = new LinkedHashMap<>(); + CACHE_CONTAINER.forEach((key, value) -> map.put(key, value.getData())); + return map; + } + @PreDestroy public void preDestroy() { log.debug("Cancelling all timer tasks"); @@ -105,7 +112,7 @@ public class InMemoryCacheStore extends AbstractStringCacheStore { clear(); } - private void clear() { + public void clear() { CACHE_CONTAINER.clear(); } diff --git a/src/main/java/run/halo/app/cache/LevelCacheStore.java b/src/main/java/run/halo/app/cache/LevelCacheStore.java index 1df727040cd17fc55b29643ba207f6b08e10af89..4a49897587c00050d4f62809a3d5c2d6ab81c6fa 100644 --- a/src/main/java/run/halo/app/cache/LevelCacheStore.java +++ b/src/main/java/run/halo/app/cache/LevelCacheStore.java @@ -5,6 +5,7 @@ import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; import java.util.Timer; @@ -117,6 +118,22 @@ public class LevelCacheStore extends AbstractStringCacheStore { log.debug("cache remove key: [{}]", key); } + @Override + public LinkedHashMap toMap() { + LinkedHashMap map = new LinkedHashMap<>(); + LEVEL_DB.forEach(entry -> { + String key = bytesToString(entry.getKey()); + String valueJson = bytesToString(entry.getValue()); + Optional> cacheWrapperOptional = jsonToCacheWrapper(valueJson); + if (cacheWrapperOptional.isPresent()) { + map.put(key, cacheWrapperOptional.get().getData()); + } else { + map.put(key, null); + } + }); + return map; + } + private byte[] stringToBytes(String str) { return str.getBytes(Charset.defaultCharset()); diff --git a/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java b/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java index 5ee993fe4408ab588365f9ada2c907437c6879d3..188a23c0deee7fa256f75294a9f66f0f74d13a48 100644 --- a/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java @@ -1,22 +1,31 @@ package run.halo.app.service.impl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import run.halo.app.cache.AbstractStringCacheStore; import run.halo.app.service.AuthorizationService; +import run.halo.app.utils.JsonUtils; /** * @author ZhiXiang Yuan + * @author guqing * @date 2021/01/21 11:28 */ +@Slf4j @Service public class AuthorizationServiceImpl implements AuthorizationService { - + private static final String ACCESS_PERMISSION_PREFIX = "ACCESS_PERMISSION: "; private final AbstractStringCacheStore cacheStore; public AuthorizationServiceImpl(AbstractStringCacheStore cacheStore) { @@ -54,6 +63,29 @@ public class AuthorizationServiceImpl implements AuthorizationService { accessStore.remove(value); cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS); + + for (Entry entry : cacheStore.toMap().entrySet()) { + String key = entry.getKey(); + if (!key.startsWith(ACCESS_PERMISSION_PREFIX)) { + continue; + } + Set valueSet = jsonToValueSet(entry.getValue()); + if (valueSet.contains(value)) { + valueSet.remove(value); + cacheStore.putAny(key, valueSet, 1, TimeUnit.DAYS); + } + } + } + + private Set jsonToValueSet(String json) { + try { + return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json, + new TypeReference>() { + }); + } catch (JsonProcessingException e) { + log.warn("Failed to convert json to authorization cache value set: [{}]", json, e); + } + return Collections.emptySet(); } private void doAuthorization(String value) { @@ -70,7 +102,7 @@ public class AuthorizationServiceImpl implements AuthorizationService { HttpServletRequest request = requestAttributes.getRequest(); - return "ACCESS_PERMISSION: " + request.getSession().getId(); + return ACCESS_PERMISSION_PREFIX + request.getSession().getId(); } } diff --git a/src/test/java/run/halo/app/cache/InMemoryCacheStoreTest.java b/src/test/java/run/halo/app/cache/InMemoryCacheStoreTest.java index f92eadba888081f2a8b060b659d8b0a98687d534..1d2f8bffc51531f20affac690243747497b65161 100644 --- a/src/test/java/run/halo/app/cache/InMemoryCacheStoreTest.java +++ b/src/test/java/run/halo/app/cache/InMemoryCacheStoreTest.java @@ -96,4 +96,24 @@ class InMemoryCacheStoreTest { // Assertion assertFalse(valueOptional.isPresent()); } + + @Test + void toMapTest() { + InMemoryCacheStore localCacheStore = new InMemoryCacheStore(); + localCacheStore.clear(); + String key1 = "test_key_1"; + String value1 = "test_value_1"; + + // Put the cache + localCacheStore.put(key1, value1); + assertEquals("{test_key_1=test_value_1}", localCacheStore.toMap().toString()); + + String key2 = "test_key_2"; + String value2 = "test_value_2"; + + // Put the cache + localCacheStore.put(key2, value2); + assertEquals("{test_key_2=test_value_2, test_key_1=test_value_1}", + localCacheStore.toMap().toString()); + } } diff --git a/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java b/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bb5b8a6f614d8d8e4a501d4b2d468911224086e9 --- /dev/null +++ b/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java @@ -0,0 +1,129 @@ +package run.halo.app.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import run.halo.app.cache.InMemoryCacheStore; +import run.halo.app.utils.JsonUtils; + +/** + * @author guqing + * @date 2021-11-19 + */ +public class AuthorizationServiceImplTest { + + private AuthorizationServiceImpl authorizationService; + private InMemoryCacheStore inMemoryCacheStore; + + @BeforeEach + public void setUp() { + inMemoryCacheStore = new InMemoryCacheStore(); + authorizationService = new AuthorizationServiceImpl(inMemoryCacheStore); + } + + @Test + public void deletePostAuthorizationTest() { + inMemoryCacheStore.clear(); + RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); + + authorizationService.postAuthorization(1); + authorizationService.postAuthorization(2); + + Set permissions = authorizationService.getAccessPermissionStore(); + assertEquals("[POST:1, POST:2]", permissions.toString()); + + authorizationService.deletePostAuthorization(1); + Set permissionsAfterDelete = authorizationService.getAccessPermissionStore(); + assertEquals("[POST:2]", permissionsAfterDelete.toString()); + + RequestContextHolder.resetRequestAttributes(); + inMemoryCacheStore.clear(); + } + + @Test + public void complexityOfDeletePostAuthorizationTest() { + inMemoryCacheStore.clear(); + // simulate session of user 1 + RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); + // user 1 accessed two encrypted posts + authorizationService.postAuthorization(1); + authorizationService.postAuthorization(2); + + // simulate session of user 2 + RequestContextHolder.setRequestAttributes(mockRequestAttributes("2")); + + // user 2 accessed two encrypted posts + authorizationService.postAuthorization(2); + authorizationService.postAuthorization(3); + + assertEquals(objectToJson(inMemoryCacheStore.toMap()), + "{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\",\\\"POST:2\\\"]\"," + + "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\",\\\"POST:2\\\"]\"}"); + + // simulate the admin user to change the post password + authorizationService.deletePostAuthorization(2); + + assertEquals(objectToJson(inMemoryCacheStore.toMap()), + "{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\"]\"," + + "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\"]\"}"); + + RequestContextHolder.resetRequestAttributes(); + inMemoryCacheStore.clear(); + } + + @Test + public void deleteCategoryAuthorizationTest() { + inMemoryCacheStore.clear(); + // simulate session of user 1 + RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); + // user 1 accessed two encrypted posts + authorizationService.categoryAuthorization(1); + authorizationService.categoryAuthorization(2); + + // simulate session of user 2 + RequestContextHolder.setRequestAttributes(mockRequestAttributes("2")); + // user 2 accessed two encrypted categories + authorizationService.categoryAuthorization(1); + authorizationService.categoryAuthorization(3); + + assertEquals(objectToJson(inMemoryCacheStore.toMap()), + "{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:3\\\"]\"," + + "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:2\\\"]\"}"); + + // simulate the admin user to change the category password of No.1 + authorizationService.deleteCategoryAuthorization(1); + + assertEquals(objectToJson(inMemoryCacheStore.toMap()), + "{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:3\\\"]\"," + + "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:2\\\"]\"}"); + + RequestContextHolder.resetRequestAttributes(); + inMemoryCacheStore.clear(); + } + + private ServletRequestAttributes mockRequestAttributes(String sessionId) { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockServletContext context = new MockServletContext(); + MockHttpSession session = new MockHttpSession(context, sessionId); + request.setSession(session); + return new ServletRequestAttributes(request); + } + + private String objectToJson(Object o) { + try { + return JsonUtils.objectToJson(o); + } catch (JsonProcessingException e) { + // ignore this + } + return StringUtils.EMPTY; + } +}