// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

//! AuthCache manages an in-memory cache of recently used short-lived authentication tokens.
#![deny(missing_docs)]

use fuchsia_zircon::{Duration, Time};
use log::{info, warn};
use std::any::Any;
use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use thiserror::Error;

/// Constant offset for the cache expiry. Tokens will only be returned from
/// the cache if they have at least this much life remaining.
const PADDING_FOR_TOKEN_EXPIRY: Duration = Duration::from_seconds(600);

/// Number of invalid entries tolerated in the expiry queue before invalid
/// entries are purged.  This is expressed as a fraction of the cache capacity.
const INVALID_ENTRY_FLUSH_THRESHOLD: f32 = 0.5;

/// An enumeration of the possible failure modes for operations on a `TokenCache`.
#[derive(Debug, PartialEq, Eq, Error)]
pub enum AuthCacheError {
    /// One or more inputs were not valid.
    #[error("invalid argument")]
    InvalidArguments,
    /// The supplied key could not be found in the cache.
    #[error("supplied key not found in cache")]
    KeyNotFound,
}

/// Trait for keys used in the cache.  `CacheKey` requires the `Any` trait for
/// dynamic typing.  As `Any` is not implemented for any struct containing a
/// non-'static reference, any valid implementation of `CacheKey` may not
/// contain references of non-'static lifetimes.
pub trait CacheKey: Any + Send + Sync {
    /// Returns the identity provider type, ex. 'google'
    fn auth_provider_type(&self) -> &str;
    /// Returns the account identifier as given by the identity provider.
    fn user_profile_id(&self) -> &str;
    /// Returns an identifier appropriate for differentiating CacheKeys of the
    /// same concrete type with the same auth_provider_type and user_profile_id.
    fn subkey(&self) -> &str;
}

impl Hash for dyn CacheKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.auth_provider_type().hash(state);
        self.user_profile_id().hash(state);
        self.subkey().hash(state);
        self.type_id().hash(state);
    }
}

impl PartialEq for dyn CacheKey {
    fn eq(&self, other: &(dyn CacheKey + 'static)) -> bool {
        self.auth_provider_type() == other.auth_provider_type()
            && self.user_profile_id() == other.user_profile_id()
            && self.subkey() == other.subkey()
            && self.type_id() == other.type_id()
    }
}

impl Eq for dyn CacheKey {}

/// Trait that specifies the concrete token type a token key can put or
/// retrieve from the cache.
pub trait KeyFor {
    /// Token type the key exclusively identifies.
    type TokenType;
}

/// Trait for tokens stored in the cache.  `CacheToken` requires the `Any`
/// trait for dynamic typing.  As `Any` is not implemented for any struct
/// containing a non-'static reference, any valid implementation of
/// `CacheToken` may not contain references of non-'static lifetimes.
pub trait CacheToken: Any + Send + Sync {
    /// Returns the time at which a token becomes invalid.
    fn expiry_time(&self) -> &Time;
}

/// An entry in the `TokenCache` expiry queue.
struct ExpiryQueueEntry {
    /// Cache entry this entry tracks.
    cache_key: Arc<dyn CacheKey>,
    /// Time at which the referenced token expires.
    expiry_time: Time,
}

impl ExpiryQueueEntry {
    /// Construct a new `ExpiryQueueEntry`.
    fn new(cache_key: Arc<dyn CacheKey>, expiry_time: Time) -> ExpiryQueueEntry {
        ExpiryQueueEntry { cache_key: cache_key, expiry_time: expiry_time }
    }
}

impl Ord for ExpiryQueueEntry {
    fn cmp(&self, other: &ExpiryQueueEntry) -> Ordering {
        self.expiry_time.cmp(&other.expiry_time).reverse()
    }
}

impl PartialOrd for ExpiryQueueEntry {
    fn partial_cmp(&self, other: &ExpiryQueueEntry) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for ExpiryQueueEntry {
    fn eq(&self, other: &ExpiryQueueEntry) -> bool {
        self.expiry_time == other.expiry_time
    }
}

impl Eq for ExpiryQueueEntry {}

/// A wrapper struct around a `CacheToken` that allows it to be used for
/// dynamic dispatch as both `CacheToken` and `Any` trait objects.
/// This approach becomes unnecessary if casting to and from a supertrait
/// (&CacheToken <-> &Any) becomes possible.  Upcasting is tracked as a
/// requirement in RFC issue https://github.com/rust-lang/rfcs/issues/349.
/// An alternative is to store Arc<CacheToken + Any> if multiple-trait trait
/// objects are added (https://github.com/rust-lang/rfcs/issues/2035)
struct TokenReference {
    /// token stored as `CacheToken` trait object
    cache_token: Arc<dyn CacheToken>,
    /// token stored as `Any` trait object
    any: Arc<dyn Any + Send + Sync>,
}

impl TokenReference {
    /// Create a new `TokenReference`
    fn new<T: CacheToken>(token: Arc<T>) -> TokenReference {
        TokenReference {
            cache_token: Arc::clone(&token) as Arc<dyn CacheToken>,
            any: token as Arc<dyn Any + Send + Sync>,
        }
    }

    /// Borrow as a `CacheToken` trait object.
    fn as_cache_token(&self) -> &Arc<dyn CacheToken> {
        &self.cache_token
    }

    /// Borrow as an `Any` trait object.
    fn as_any(&self) -> &Arc<dyn Any + Send + Sync> {
        &self.any
    }
}

/// A cache of recently used authentication tokens. All tokens contain an
/// expiry time and are removed when this time is reached.
pub struct TokenCache {
    /// A mapping holding cached tokens of arbitrary types.
    token_map: HashMap<Arc<dyn CacheKey>, TokenReference>,
    /// A priority queue used to evict expired tokens.
    expiry_queue: BinaryHeap<ExpiryQueueEntry>,
    /// Maximum number of tokens the cache will hold.
    capacity: usize,
    /// `Time` of the last cache operation.  Used to validate time progression.
    last_time: Time,
}

impl TokenCache {
    /// Creates a new `TokenCache` with the specified capacity.
    pub fn new(capacity: usize) -> TokenCache {
        let expiry_queue_capacity = capacity + Self::num_tolerated_invalid_entries(capacity);
        TokenCache {
            token_map: HashMap::with_capacity(capacity),
            expiry_queue: BinaryHeap::with_capacity(expiry_queue_capacity),
            capacity: capacity,
            last_time: Self::get_current_time(),
        }
    }

    /// Sanity check that system time has not jumped backwards since last time this method was
    /// called. If time jumped backwards, a warning is logged and the entire cache is cleared.
    /// If time is normal, this method does nothing.
    /// TODO(dnordstrom): Long term solution involving time service or monotonic clocks.
    fn validate_time_progression(&mut self) {
        let current_time = Self::get_current_time();
        if current_time < self.last_time {
            warn!(
                "time jumped backwards from {:?} to {:?}, clearing {:?} token cache entries",
                self.last_time,
                current_time,
                self.token_map.len(),
            );
            self.token_map.clear();
            self.expiry_queue.clear();
        }
        self.last_time = current_time;
    }

    /// Returns a token for the specified key if present and not expired.
    pub fn get<K, V>(&mut self, key: &K) -> Option<Arc<V>>
    where
        K: CacheKey + KeyFor<TokenType = V>,
        V: CacheToken,
    {
        self.validate_time_progression();
        self.evict_expired();

        let uncast_token = self.token_map.get(key as &dyn CacheKey)?.as_any();
        if let Ok(downcast_token) = Arc::clone(uncast_token).downcast::<V>() {
            Some(downcast_token)
        } else {
            warn!("Error downcasting token in cache.");
            None
        }
    }

    /// Adds a token to the cache, replacing any existing token for the same key.
    pub fn put<K, V>(&mut self, key: K, token: Arc<V>)
    where
        K: CacheKey + KeyFor<TokenType = V>,
        V: CacheToken,
    {
        self.validate_time_progression();
        self.evict_expired();
        if self.token_map.len() == self.capacity
            && !self.token_map.contains_key(&key as &dyn CacheKey)
        {
            self.evict_random();
        }

        let arc_key = Arc::new(key) as Arc<dyn CacheKey>;
        self.expiry_queue.push(ExpiryQueueEntry::new(Arc::clone(&arc_key), *token.expiry_time()));
        self.token_map.insert(arc_key, TokenReference::new(token));

        self.flush_invalid();
    }

    /// Deletes all the tokens associated with the given auth_provider_type and
    /// user_profile_id.  Returns an error if no matching, unexpired tokens are found.
    pub fn delete_matching(
        &mut self,
        auth_provider_type: &str,
        user_profile_id: &str,
    ) -> Result<(), AuthCacheError> {
        self.validate_time_progression();
        self.evict_expired();

        let entries_before_delete = self.token_map.len();
        self.token_map.retain(|key, _| {
            key.auth_provider_type() != auth_provider_type
                || key.user_profile_id() != user_profile_id
        });
        self.flush_invalid();

        let entries_after_delete = self.token_map.len();
        if entries_after_delete < entries_before_delete {
            info!(
                "Deleted {:?} matching entries from the token cache.",
                entries_before_delete - entries_after_delete
            );
            Ok(())
        } else {
            Err(AuthCacheError::KeyNotFound)
        }
    }

    /// Evicts all expired tokens from the cache.  Invalid entries in the expiry
    /// queue are silently discarded.
    fn evict_expired(&mut self) {
        let current_time = Self::get_current_time();
        while let Some(expiry_entry) = pop_if(&mut self.expiry_queue, |entry| {
            current_time > (entry.expiry_time - PADDING_FOR_TOKEN_EXPIRY)
        }) {
            let ExpiryQueueEntry { cache_key, expiry_time } = expiry_entry;
            match self.token_map.get(&cache_key) {
                Some(token) if *token.as_cache_token().expiry_time() == expiry_time => {
                    self.token_map.remove(&cache_key);
                }
                _ => (), // silently discard if key doesn't exist or expiry has been updated
            }
        }
    }

    /// Evicts a random token from the cache.
    fn evict_random(&mut self) {
        // TODO(satsukiu): Use a method to get a key that is actually random.
        match self.token_map.keys().next().map(|arc| arc.clone()) {
            Some(random_key) => {
                self.token_map.remove(&random_key);
            }
            None => warn!("Tried to evict random token from empty cache."),
        };
    }

    /// Flushes invalid entries from the eviction queue if there are excessive
    /// numbers of invalid entries.  Entries are invalid if the corresponding
    /// token has been updated with a new expiration time or the token has been
    /// removed from the cache.
    fn flush_invalid(&mut self) {
        let num_invalid_entries = self.expiry_queue.len() - self.token_map.len();
        if num_invalid_entries >= Self::num_tolerated_invalid_entries(self.capacity) {
            // This rebuilds the queue, which has the same end result as
            // filtering out invalid entries but is much slower.
            // TODO(satsukiu): flush out invalid entries inplace in the queue.
            self.expiry_queue.clear();
            self.expiry_queue.extend(self.token_map.iter().map(|(key, token)| {
                ExpiryQueueEntry::new(Arc::clone(key), *token.as_cache_token().expiry_time())
            }));
        }
    }

    /// Defines the number of invalid entries allowed in the expiry queue.
    fn num_tolerated_invalid_entries(capacity: usize) -> usize {
        ((capacity as f32) * INVALID_ENTRY_FLUSH_THRESHOLD) as usize
    }

    fn get_current_time() -> Time {
        fuchsia_runtime::utc_time()
    }
}

/// Pop and return top element in a heap if it passes some condition.
fn pop_if<T, F>(heap: &mut BinaryHeap<T>, condition: F) -> Option<T>
where
    T: Ord,
    F: FnOnce(&T) -> bool,
{
    match condition(heap.peek()?) {
        true => heap.pop(),
        false => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const CACHE_SIZE: usize = 10;

    const TEST_AUTH_PROVIDER: &str = "test_auth_provider";
    const TEST_USER_ID: &str = "test_auth_provider/profiles/user";
    const TEST_TOKEN_CONTENTS: &str = "Token contents";
    const LONG_EXPIRY: Duration = Duration::from_seconds(3000);
    const ALREADY_EXPIRED: Duration = Duration::from_seconds(1);

    #[derive(Clone, Debug, PartialEq, Eq)]
    struct TestToken {
        expiry_time: Time,
        token: String,
    }

    impl CacheToken for TestToken {
        fn expiry_time(&self) -> &Time {
            &self.expiry_time
        }
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    struct TestKey {
        auth_provider_type: String,
        user_profile_id: String,
        audience: String,
    }

    impl CacheKey for TestKey {
        fn auth_provider_type(&self) -> &str {
            &self.auth_provider_type
        }

        fn user_profile_id(&self) -> &str {
            &self.user_profile_id
        }

        fn subkey(&self) -> &str {
            &self.audience
        }
    }

    impl KeyFor for TestKey {
        type TokenType = TestToken;
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    struct AlternateTestToken {
        expiry_time: Time,
        metadata: Vec<String>,
        token: String,
    }

    impl CacheToken for AlternateTestToken {
        fn expiry_time(&self) -> &Time {
            &self.expiry_time
        }
    }

    #[derive(Clone, Debug, PartialEq, Eq)]
    struct AlternateTestKey {
        auth_provider_type: String,
        user_profile_id: String,
        scopes: String,
    }

    impl CacheKey for AlternateTestKey {
        fn auth_provider_type(&self) -> &str {
            &self.auth_provider_type
        }

        fn user_profile_id(&self) -> &str {
            &self.user_profile_id
        }

        fn subkey(&self) -> &str {
            &self.scopes
        }
    }

    impl KeyFor for AlternateTestKey {
        type TokenType = AlternateTestToken;
    }

    fn build_test_key(user_suffix: &str, audience: &str) -> TestKey {
        TestKey {
            auth_provider_type: TEST_AUTH_PROVIDER.to_string(),
            user_profile_id: TEST_USER_ID.to_string() + user_suffix,
            audience: audience.to_string(),
        }
    }

    fn build_alternate_test_key(user_suffix: &str, scopes: &str) -> AlternateTestKey {
        AlternateTestKey {
            auth_provider_type: TEST_AUTH_PROVIDER.to_string(),
            user_profile_id: TEST_USER_ID.to_string() + user_suffix,
            scopes: scopes.to_string(),
        }
    }

    fn build_test_token(time_until_expiry: Duration, suffix: &str) -> Arc<TestToken> {
        Arc::new(TestToken {
            expiry_time: fuchsia_runtime::utc_time() + time_until_expiry,
            token: TEST_TOKEN_CONTENTS.to_string() + suffix,
        })
    }

    fn build_alternate_test_token(
        time_until_expiry: Duration,
        suffix: &str,
    ) -> Arc<AlternateTestToken> {
        Arc::new(AlternateTestToken {
            expiry_time: fuchsia_runtime::utc_time() + time_until_expiry,
            token: TEST_TOKEN_CONTENTS.to_string() + suffix,
            metadata: vec![suffix.to_string()],
        })
    }

    #[test]
    fn test_get_and_put_token() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);

        // Verify requesting an entry from an cache that does not contain it fails.
        let key_1 = build_test_key("", "audience_1");
        assert_eq!(token_cache.get(&key_1), None);

        // Verify inserting then retrieving a token succeeds.
        let token_1 = build_test_token(LONG_EXPIRY, "1");
        token_cache.put(key_1.clone(), token_1.clone());
        assert_eq!(token_cache.get(&key_1), Some(token_1.clone()));

        // Verify a second token can be stored without conflict.
        let key_2 = build_test_key("", "audience_2");
        let token_2 = build_test_token(LONG_EXPIRY, "2");
        assert_eq!(token_cache.get(&key_2), None);
        token_cache.put(key_2.clone(), token_2.clone());
        assert_eq!(token_cache.get(&key_2), Some(token_2));
        assert_eq!(token_cache.get(&key_1), Some(token_1));
    }

    #[test]
    fn test_get_and_put_duplicate_key() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        let key = build_test_key("", "key");
        let original_token = build_test_token(LONG_EXPIRY, "original");
        token_cache.put(key.clone(), original_token.clone());
        assert_eq!(token_cache.get(&key), Some(original_token));

        // Verify most recently inserted token is returned.
        let duplicate_token = build_test_token(LONG_EXPIRY, "duplicate");
        token_cache.put(key.clone(), duplicate_token.clone());
        assert_eq!(token_cache.get(&key), Some(duplicate_token));

        // Verify no token returned if most recently inserted is expired.
        token_cache.put(key.clone(), build_test_token(ALREADY_EXPIRED, "expired"));
        assert!(token_cache.get(&key).is_none());
    }

    #[test]
    fn test_get_and_put_multiple_token_types() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);

        // Verify cache will get and put different datatypes
        let key = build_test_key("", "audience_1");
        let token = build_test_token(LONG_EXPIRY, "1");
        token_cache.put(key.clone(), token.clone());
        let alternate_key = build_alternate_test_key("", "scope");
        let alternate_token = build_alternate_test_token(LONG_EXPIRY, "2");
        token_cache.put(alternate_key.clone(), alternate_token.clone());
        assert_eq!(token_cache.get(&key), Some(token));
        assert_eq!(token_cache.get(&alternate_key), Some(alternate_token));

        // Verify keys of identical contents but different types do not clash.
        let key_2 = build_test_key("", "clash-test");
        let token_2 = build_test_token(LONG_EXPIRY, "3");
        token_cache.put(key_2.clone(), token_2.clone());
        let alternate_key_2 = build_alternate_test_key("", "clash-test");
        let alternate_token_2 = build_alternate_test_token(LONG_EXPIRY, "4");
        token_cache.put(alternate_key_2.clone(), alternate_token_2.clone());

        assert_eq!(token_cache.get(&key_2), Some(token_2));
        assert_eq!(token_cache.get(&alternate_key_2), Some(alternate_token_2));
    }

    #[test]
    fn test_delete_matching() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        let key = build_test_key("", "audience");

        // Verify deleting when cache is empty fails.
        assert_eq!(
            token_cache.delete_matching(key.auth_provider_type(), key.user_profile_id()),
            Err(AuthCacheError::KeyNotFound)
        );

        // Verify matching keys are removed.
        token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, ""));
        assert_eq!(
            token_cache.delete_matching(key.auth_provider_type(), key.user_profile_id()),
            Ok(())
        );
        assert!(token_cache.get(&key).is_none());

        // Verify non-matching keys are not removed.
        let matching_key = build_test_key("matching", "");
        let non_matching_key = build_test_key("non-matching", "");
        let non_matching_token = build_test_token(LONG_EXPIRY, "non-matching");
        token_cache.put(matching_key.clone(), build_test_token(LONG_EXPIRY, "matching"));
        token_cache.put(non_matching_key.clone(), non_matching_token.clone());
        assert_eq!(
            token_cache
                .delete_matching(matching_key.auth_provider_type(), matching_key.user_profile_id()),
            Ok(())
        );
        assert!(token_cache.get(&matching_key).is_none());
        assert_eq!(token_cache.get(&non_matching_key), Some(non_matching_token));
    }

    #[test]
    fn test_expired_tokens_evicted() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);

        // Insert one entry that's already expired and one that hasn't.
        let key = build_test_key("valid", "audience_1");
        let token = build_test_token(LONG_EXPIRY, "1");
        token_cache.put(key.clone(), token.clone());
        let expired_key = build_alternate_test_key("expired", "scope");
        let expired_token = build_alternate_test_token(ALREADY_EXPIRED, "2");
        token_cache.put(expired_key.clone(), expired_token.clone());

        // Getting the expired key should fail.
        assert_eq!(token_cache.get(&key), Some(token));
        assert_eq!(token_cache.get(&expired_key), None);
    }

    #[test]
    fn test_cache_put_at_capacity() {
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        // Populate cache to capacity
        let original_keys: Vec<TestKey> =
            (0..CACHE_SIZE).map(|key_num| build_test_key(&key_num.to_string(), "")).collect();
        for key in original_keys.iter() {
            token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, ""));
        }

        // Add one token past capacity
        let new_key = build_test_key("extra", "");
        token_cache.put(new_key.clone(), build_test_token(LONG_EXPIRY, ""));

        // New token should be present, one random original token should be evicted.
        assert!(token_cache.get(&new_key).is_some());
        assert_eq!(original_keys.iter().filter(|key| token_cache.get(*key).is_none()).count(), 1);
        assert_eq!(token_cache.token_map.len(), CACHE_SIZE);

        // Adding a duplicate key when cache is at capacity shouldn't evict anything.
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        for key in original_keys.iter() {
            token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, ""));
        }
        let dup_key = original_keys.iter().next().unwrap().clone();
        token_cache.put(dup_key, build_test_token(LONG_EXPIRY, "duplicate"));
        assert!(original_keys.iter().all(|key| token_cache.get(key).is_some()));
        assert_eq!(token_cache.token_map.len(), CACHE_SIZE);
    }

    #[test]
    fn test_cache_enforces_maximum_size() {
        let keys: Vec<TestKey> =
            (0..CACHE_SIZE).map(|key_num| build_test_key(&key_num.to_string(), "")).collect();

        // Verify cache after forcing eviction queue flush by replacing every key.
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        for key in keys.iter() {
            token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, "original"));
        }
        for key in keys.iter() {
            token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, "overwrite"));
        }
        assert_eq!(token_cache.token_map.len(), CACHE_SIZE);
        assert!(keys.iter().all(|key| token_cache.get(key).is_some()));

        // Verify cache contains only CACHE_SIZE entries after trying to overfill cache.
        let mut token_cache = TokenCache::new(CACHE_SIZE);
        let too_many_keys: Vec<TestKey> =
            (0..CACHE_SIZE * 2).map(|key_num| build_test_key(&key_num.to_string(), "")).collect();
        for key in too_many_keys.iter() {
            token_cache.put(key.clone(), build_test_token(LONG_EXPIRY, ""));
        }
        assert_eq!(token_cache.token_map.len(), CACHE_SIZE);
        assert_eq!(
            too_many_keys.iter().filter(|key| token_cache.get(*key).is_some()).count(),
            CACHE_SIZE
        );
    }
}
