diff --git a/src/main/java/org/casbin/jcasbin/main/SyncedCachedEnforcer.java b/src/main/java/org/casbin/jcasbin/main/SyncedCachedEnforcer.java new file mode 100644 index 0000000..6de7b6d --- /dev/null +++ b/src/main/java/org/casbin/jcasbin/main/SyncedCachedEnforcer.java @@ -0,0 +1,334 @@ +// Copyright 2024 The casbin Authors. All Rights Reserved. +// +// 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. + +package org.casbin.jcasbin.main; + +import org.casbin.jcasbin.model.Model; +import org.casbin.jcasbin.persist.Adapter; +import org.casbin.jcasbin.persist.cache.Cache; +import org.casbin.jcasbin.persist.cache.CacheableParam; +import org.casbin.jcasbin.persist.cache.DefaultCache; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class SyncedCachedEnforcer extends SyncedEnforcer{ + private Duration expireTime; + private Cache cache; + private final AtomicBoolean enableCache = new AtomicBoolean(true); + private final static ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); + + /** + * Default constructor. Initializes a new SyncedCachedEnforcer with a default cache. + */ + public SyncedCachedEnforcer(){ + super(); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model file and a policy file. + * + * @param modelPath The path of the model file. + * @param policyFile The path of the policy file. + */ + public SyncedCachedEnforcer(String modelPath, String policyFile){ + super(modelPath, policyFile); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model file and a database adapter. + * + * @param modelPath The path of the model file. + * @param adapter The adapter for the database. + */ + public SyncedCachedEnforcer(String modelPath, Adapter adapter) { + super(modelPath, adapter); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model and a database adapter. + * + * @param m The model. + * @param adapter The adapter for the database. + */ + public SyncedCachedEnforcer(Model m, Adapter adapter) { + super(m, adapter); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model. + * + * @param m The model. + */ + public SyncedCachedEnforcer(Model m) { + super(m); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model file. + * + * @param modelPath The path of the model file. + */ + public SyncedCachedEnforcer(String modelPath) { + super(modelPath); + this.cache = new DefaultCache(); + } + + /** + * Initializes an enforcer with a model file, a policy file, and a logging flag. + * + * @param modelPath The path of the model file. + * @param policyFile The path of the policy file. + * @param enableLog Whether to enable logging for Casbin. + */ + public SyncedCachedEnforcer(String modelPath, String policyFile, boolean enableLog) { + super(modelPath, policyFile, enableLog); + this.cache = new DefaultCache(); + } + + /** + * Enables or disables caching. + * + * @param enable Whether to enable caching. + */ + public void enableCache(boolean enable) { + enableCache.set(enable); + } + + /** + * Performs an enforcement check based on given parameters, using the cache. + * + * @param rvals Parameters for the enforcement check. + * @return The result of the enforcement check. + */ + public boolean enforce(Object... rvals) { + if (enableCache.get()) { + return super.enforce(rvals); + } + + String key = getKey(rvals); + if (key == null) { + return super.enforce(rvals); + } + + Boolean cachedResult = getCachedResult(key); + if (cachedResult) { + return cachedResult; + } + + boolean result = super.enforce(rvals); + setCachedResult(key, result, expireTime); + return result; + } + + /** + * Loads the policy, clearing the cache if enabled. + */ + public void loadPolicy() { + if(enableCache == null || !enableCache.get()){ + super.loadPolicy(); + } else { + if (enableCache.get()) { + cache.clear(); + } + super.loadPolicy(); + } + } + + /** + * Adds a single policy while checking and removing the cache. + * + * @param params Policy parameters. + * @return Whether the addition was successful. + */ + public boolean addPolicy(String... params) { + if (!checkOneAndRemoveCache(params)) { + return false; + } + return super.addPolicy(params); + } + + /** + * Adds multiple policies while checking and removing the cache. + * + * @param rules Policy rules. + * @return Whether the addition was successful. + */ + public boolean addPolicies(List> rules) { + if (!checkManyAndRemoveCache(rules)) { + return false; + } + return super.addPolicies(rules); + } + + /** + * Removes a single policy while checking and removing the cache. + * + * @param params Policy parameters. + * @return Whether the removal was successful. + */ + public boolean removePolicy(String... params) { + if (!checkOneAndRemoveCache(params)) { + return false; + } + return super.removePolicy(params); + } + + /** + * Removes multiple policies while checking and removing the cache. + * + * @param rules Policy rules. + * @return Whether the removal was successful. + */ + public boolean removePolicies(List>rules) { + if (!checkManyAndRemoveCache(rules)) { + return false; + } + return super.removePolicies(rules); + } + + /** + * Retrieves a cached result based on the given key. + * + * @param key The cache key. + * @return The cached result. + */ + private Boolean getCachedResult(String key) { + try { + READ_WRITE_LOCK.readLock().lock(); + return cache.get(key); + } finally { + READ_WRITE_LOCK.readLock().unlock(); + } + } + + /** + * Sets the cache expiration time. + * + * @param expireTime The expiration time. + */ + public void setExpireTime(Duration expireTime) { + READ_WRITE_LOCK.writeLock().lock(); + try { + this.expireTime = expireTime; + } finally { + READ_WRITE_LOCK.writeLock().unlock(); + } + } + + /** + * Sets a custom cache. + * + * @param cache The custom cache. + */ + public void setCache(Cache cache) { + READ_WRITE_LOCK.writeLock().lock(); + try { + this.cache = cache; + } finally { + READ_WRITE_LOCK.writeLock().unlock(); + } + } + + /** + * Sets the cached result. + * + * @param key The cache key. + * @param result The enforcement check result. + * @param extra Additional parameters. + */ + private void setCachedResult(String key, boolean result, Object... extra) { + cache.set(key, result, extra); + } + + /** + * Retrieves a cache key from the given parameters. + * + * @param params The parameters for generating the key. + * @return The generated cache key as a string. + */ + public String getCacheKey(Object... params) { + StringBuilder key = new StringBuilder(); + for (Object param : params) { + if (param instanceof String) { + key.append((String) param); + } else if (param instanceof CacheableParam) { + key.append(((CacheableParam) param).getCacheKey()); + } else { + return ""; + } + key.append("$$"); + } + return key.toString(); + } + + /** + * Generates a key based on the given parameters. + * + * @param params Parameters. + * @return The generated key. + */ + private String getKey(Object... params) { + return getCacheKey(params); + } + + /** + * Invalidates the cache by clearing it. + */ + public void invalidateCache() { + cache.clear(); + } + + /** + * Checks and removes cache for a single policy. + * + * @param params Policy parameters. + * @return Whether the check was successful. + */ + private boolean checkOneAndRemoveCache(String... params) { + if (enableCache.get()) { + String key = getKey((Object) params); + if (key != null) { + cache.delete(key); + } + } + return true; + } + + /** + * Checks and removes cache for multiple policies. + * + * @param rules Policy rules. + * @return Whether the check was successful. + */ + private boolean checkManyAndRemoveCache(List> rules) { + if (!rules.isEmpty() && enableCache.get()) { + for (List rule : rules) { + String key = getKey(rule); + if (key != null) { + cache.delete(key); + } + } + } + return true; + } +} diff --git a/src/test/java/org/casbin/jcasbin/main/SyncedCachedEnforcerUnitTest.java b/src/test/java/org/casbin/jcasbin/main/SyncedCachedEnforcerUnitTest.java new file mode 100644 index 0000000..63d4ff3 --- /dev/null +++ b/src/test/java/org/casbin/jcasbin/main/SyncedCachedEnforcerUnitTest.java @@ -0,0 +1,85 @@ +// Copyright 2024 The casbin Authors. All Rights Reserved. +// +// 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. + +package org.casbin.jcasbin.main; + +import org.junit.Test; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; + +public class SyncedCachedEnforcerUnitTest { + + private void testSyncEnforceCache(SyncedCachedEnforcer e, String sub, Object obj, String act, boolean res) { + boolean myRes = e.enforce(sub, obj, act); + assertEquals(String.format("%s, %s, %s: %b, supposed to be %b", sub, obj, act, myRes, res), res, myRes); + } + + @Test + public void testSyncCache() throws Exception { + final SyncedCachedEnforcer enforcer = new SyncedCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv"); + enforcer.setExpireTime(Duration.ofMillis(1)); + + int goThread = 1000; + CountDownLatch latch = new CountDownLatch(goThread); + + for (int i = 0; i < goThread; i++) { + new Thread(() -> { + enforcer.addPolicy("alice", "data2", "read"); + testSyncEnforceCache(enforcer, "alice", "data2", "read", true); + enforcer.invalidateCache(); + latch.countDown(); + }).start(); + } + latch.await(); + + enforcer.removePolicy("alice", "data2", "read"); + + testSyncEnforceCache(enforcer, "alice", "data1", "read", true); + TimeUnit.MILLISECONDS.sleep(2); + testSyncEnforceCache(enforcer, "alice", "data1", "read", true); + + testSyncEnforceCache(enforcer, "alice", "data1", "write", false); + testSyncEnforceCache(enforcer, "alice", "data2", "read", false); + testSyncEnforceCache(enforcer, "alice", "data2", "write", false); + + enforcer.removePolicy("alice", "data1", "read"); + + testSyncEnforceCache(enforcer, "alice", "data1", "read", false); + testSyncEnforceCache(enforcer, "alice", "data1", "write", false); + testSyncEnforceCache(enforcer, "alice", "data2", "read", false); + testSyncEnforceCache(enforcer, "alice", "data2", "write", false); + + + SyncedCachedEnforcer syncedCachedEnforcer = new SyncedCachedEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv"); + + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data1", "read", true); + testSyncEnforceCache(syncedCachedEnforcer, "bob", "data2", "write", true); + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data2", "read", true); + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data2", "write", true); + + syncedCachedEnforcer.removePolicies(new String[][]{ + {"alice", "data1", "read"}, + {"bob", "data2", "write"}, + }); + + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data1", "read", false); + testSyncEnforceCache(syncedCachedEnforcer, "bob", "data2", "write", false); + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data2", "read", true); + testSyncEnforceCache(syncedCachedEnforcer, "alice", "data2", "write", true); + } +}