All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cesecore.authorization.AuthorizationCache Maven / Gradle / Ivy

/*************************************************************************
 *                                                                       *
 *  CESeCore: CE Security Core                                           *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.cesecore.authorization;

import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Logger;
import org.cesecore.authentication.AuthenticationFailedException;
import org.cesecore.authentication.tokens.AuthenticationToken;
import org.cesecore.authorization.access.AuthorizationCacheReload;
import org.cesecore.authorization.access.AuthorizationCacheReloadListener;
import org.cesecore.util.ValidityDate;

/**
 * Cache of the authorization granted to different AuthenticationTokens.
 * 
 * Features:
 * - Concurrent cache misses for the same AuthenticationToken will only lead to a single call-back while the other threads wait
 * - Never return stale entries (when signaled that newer data might be available)
 * - Supports background reload via the rebuild(...) method which also purges unused entries
 * 
 * @version $Id: AuthorizationCache.java 25694 2017-04-13 15:28:04Z jeklund $
 */
public enum AuthorizationCache {
    INSTANCE, RAINSTANCE;
    
    private final Logger log = Logger.getLogger(AuthorizationCache.class);

    /** The access available to an authentication token and corresponding version of the authorization systems updateNumber */
    public static class AuthorizationResult {
        final HashMap accessRules;
        final int updateNumber;

        public AuthorizationResult(final HashMap accessRules, final int updateNumber) {
            this.accessRules = accessRules;
            this.updateNumber = updateNumber;
        }

        public HashMap getAccessRules() { return accessRules; }
        public int getUpdateNumeber() { return updateNumber; }
    }
    
    /** Call-back interface for loading access rules on cache miss */
    public interface AuthorizationCacheCallback {
        /** @return the access rules and corresponding update number for the specified authenticationToken  */
        AuthorizationResult loadAuthorization(AuthenticationToken authenticationToken) throws AuthenticationFailedException;
        
        /** @return the number of milliseconds to keep cache entries for after an authentication token was last seen */
        long getKeepUnusedEntriesFor();

        /** Invoked by cache on first cache miss to start listening to authorization updates */
        void subscribeToAuthorizationCacheReload(AuthorizationCacheReloadListener authorizationCacheReloadListener);
    }
    
    private class AuthorizationCacheEntry {
        HashMap accessRules;
        int updateNumber = 0;
        long timeOfLastUse = 0L;
        AuthenticationToken authenticationToken;
        final CountDownLatch countDownLatch = new CountDownLatch(1);
    }
    
    private ConcurrentHashMap cacheMap = new ConcurrentHashMap<>();
    private AtomicInteger latestUpdateNumber = new AtomicInteger(0);

    private final AtomicBoolean authorizationCacheReloadListenerRegistered = new AtomicBoolean(false);
    private final AuthorizationCacheReloadListener authorizationCacheReloadListener = new AuthorizationCacheReloadListener() {
        @Override
        public void onReload(final AuthorizationCacheReload event) {
            setUpdateNumberIfLower(event.getAccessTreeUpdateNumber());
        }
        @Override
        public String getListenerName() {
            return AuthorizationCache.class.getSimpleName();
        }
    };
    
    public void clear(final int updateNumber) {
        setUpdateNumberIfLower(updateNumber);
        cacheMap.clear();
    }

    public void clearWhenStale(final int updateNumber) {
        if (setUpdateNumberIfLower(updateNumber)) {
            cacheMap.clear();
        }
    }

    /** Full reset should only be invoked by JUnit tests */
    protected void reset() {
        cacheMap.clear();
        latestUpdateNumber.set(0);
        authorizationCacheReloadListenerRegistered.set(false);
    }

    /** Re-build the authorization cache for all entries that been seen recently (as determined by authorizationCacheCallback.getKeepUnusedEntriesFor()). */
    public void refresh(final AuthorizationCacheCallback authorizationCacheCallback, final int refreshUpdateNumber) {
        //final int refreshUpdateNumber = authorizationCacheCallback.getUpdateNumber();
        if (log.isTraceEnabled()) {
            log.trace("Starting cache refresh when update number was " + refreshUpdateNumber + ".");
        }
        setUpdateNumberIfLower(refreshUpdateNumber);
        final long purgeUnusedAuthorizationAfter = authorizationCacheCallback.getKeepUnusedEntriesFor();
        final long now = System.currentTimeMillis();
        final HashSet existingKeysWhenInvoked = new HashSet<>(cacheMap.keySet());
        for (final String key : existingKeysWhenInvoked) {
            final AuthorizationCacheEntry entry = cacheMap.get(key);
            if (entry!=null) {
                if (entry.updateNumber get(final AuthenticationToken authenticationToken, final AuthorizationCacheCallback authorizationCacheCallback) throws AuthenticationFailedException {
        return getAuthorizationResult(authenticationToken, authorizationCacheCallback).accessRules;
    }

    /** @return the access rules granted to the specified authenticationToken and corresponding update number using the callback to load them if needed. Never null.  */
    public AuthorizationResult getAuthorizationResult(final AuthenticationToken authenticationToken, final AuthorizationCacheCallback authorizationCacheCallback) throws AuthenticationFailedException {
        if (authenticationToken==null || authorizationCacheCallback==null) {
            return new AuthorizationResult(new HashMap(), 0);
        }
        final String key = authenticationToken.getUniqueId();
        final AuthorizationCacheEntry authorizationCacheEntry = new AuthorizationCacheEntry();
        AuthorizationCacheEntry ret = cacheMap.putIfAbsent(key, authorizationCacheEntry);
        if (ret == null) {
            // Start subscribing to authorization system updates on first cache miss (which happens on application startup)
            if (!authorizationCacheReloadListenerRegistered.getAndSet(true)) {
                authorizationCacheCallback.subscribeToAuthorizationCacheReload(authorizationCacheReloadListener);
            }
            ret = authorizationCacheEntry;
            try {
                ret.authenticationToken = authenticationToken;
                final AuthorizationResult authorizationResult = authorizationCacheCallback.loadAuthorization(authenticationToken);
                ret.updateNumber = authorizationResult.updateNumber;
                setUpdateNumberIfLower(ret.updateNumber);
                ret.accessRules = new HashMap<>();
                if (authorizationResult.accessRules != null) {
                    // Cache a copy of the loaded access rules map
                    ret.accessRules.putAll(authorizationResult.accessRules);
                }
            } finally {
                // Ensure that we release any waiting thread
                ret.countDownLatch.countDown();
            }
            if (log.isDebugEnabled()) {
                log.debug("Added entry for key '" + key + "'.");
            }
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Cache hit for key '" + key + "'.");
            }
            try {
                // Block while it is loading (if it is still loading)
                ret.countDownLatch.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            // Check if the returned entry is stale
            if (ret.updateNumber




© 2015 - 2024 Weber Informatics LLC | Privacy Policy