// 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.

#[macro_use]
mod macros;
pub mod file;
pub mod mem;
mod serializer;
/// FIXME remove when https://github.com/rust-lang/rust/issues/57264 is fixed
pub use self::serializer::JsonSerializer;

use failure::{format_err, Error, Fail};
use std::result;

pub type Result<T> = result::Result<T, AuthDbError>;

/// Errors that may result from operations on an AuthDb.
#[derive(Debug, Fail)]
pub enum AuthDbError {
    /// An illegal input argument was supplied, such as an invalid path.
    #[fail(display = "invalid argument")]
    InvalidArguments,
    /// A lower level failure occurred while serializing and writing the data.
    /// See logs for more information.
    #[fail(display = "unexpected error serializing or deserializing the database")]
    SerializationError,
    /// A lower level failure occurred while reading and deserializing the data.
    #[fail(display = "unexpected IO error accessing the database: {}", _0)]
    IoError(#[cause] std::io::Error),
    /// The existing contents of the DB are not valid. This could be caused by a change in file
    /// format or by data corruption.
    #[fail(display = "database contents could not be parsed")]
    DbInvalid,
    /// The requested credential is not present in the DB.
    #[fail(display = "credential not found")]
    CredentialNotFound,
}

/// Unique identifier for a user credential as stored in the auth db.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct CredentialKey {
    /// The type string for the auth provider that created this credential, inferring both the
    /// identity provider and the implementation used for authentication.
    auth_provider_type: String,
    /// A string identifier provided by the identity provider, typically the user's email address
    /// or profile url.
    user_profile_id: String,
}

impl CredentialKey {
    /// Create a new CredentialKey, or returns an Error if any input is empty.
    pub fn new(
        auth_provider_type: String,
        user_profile_id: String,
    ) -> result::Result<CredentialKey, Error> {
        if auth_provider_type.is_empty() {
            Err(format_err!("auth_provider_type cannot be empty"))
        } else if user_profile_id.is_empty() {
            Err(format_err!("user_profile_id cannot be empty"))
        } else {
            Ok(CredentialKey { auth_provider_type, user_profile_id })
        }
    }

    /// Gets the current value of the `auth_provider_type` field.
    pub fn auth_provider_type(&self) -> &str {
        &self.auth_provider_type
    }

    /// Gets the current value of the `user_profile_id` field.
    pub fn user_profile_id(&self) -> &str {
        &self.user_profile_id
    }
}

/// The set of data to be stored for a user credential.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct CredentialValue {
    /// A unique identifier for this credential including the IdP and account.
    credential_key: CredentialKey,
    /// An OAuth refresh token.
    refresh_token: String,
    /// A DER-encoded private key for signing requests that use this credential, if the credential
    /// is bound to a key pair.
    private_key: Option<Vec<u8>>,
}

impl CredentialValue {
    /// Create a new CredentialValue, or returns an Error if any input is empty.
    pub fn new(
        auth_provider_type: String,
        user_profile_id: String,
        refresh_token: String,
        private_key: Option<Vec<u8>>,
    ) -> result::Result<CredentialValue, Error> {
        if refresh_token.is_empty() {
            Err(format_err!("refresh_token cannot be empty"))
        } else {
            Ok(CredentialValue {
                credential_key: CredentialKey::new(auth_provider_type, user_profile_id)?,
                refresh_token,
                private_key,
            })
        }
    }
}

/// A trait expressing the functionality that all auth databases must provide.
pub trait AuthDb {
    /// Adds a new user credential to the database. The operation may insert a new user credential
    /// or replace an existing user credential. Replacement of an existing credential is useful
    /// when the credential has been expired or invalidated by the identity provider.
    fn add_credential(&mut self, credential: CredentialValue) -> Result<()>;

    /// Deletes the specified existing user credential from the database.
    fn delete_credential(&mut self, credential_key: &CredentialKey) -> Result<()>;

    /// Returns keys for all the credentials provisioned in this instance of the database.
    fn get_all_credential_keys<'a>(&'a self) -> Result<Vec<&'a CredentialKey>>;

    /// Returns the refresh token for a specified user credential.
    fn get_refresh_token<'a>(&'a self, credential_key: &CredentialKey) -> Result<&'a str>;
}

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

    const TEST_AUTH_PROVIDER: &str = "test_iot";
    const TEST_ID: &str = "user@test.com";
    const TEST_REFRESH_TOKEN: &str = "123456789@#$&*(%)_@(&";
    const TEST_PRIVATE_KEY: &[u8] = &[9, 8, 7, 6, 5];

    #[test]
    fn test_new_valid_credential() {
        let cred = CredentialValue::new(
            TEST_AUTH_PROVIDER.to_string(),
            TEST_ID.to_string(),
            TEST_REFRESH_TOKEN.to_string(),
            None,
        )
        .unwrap();
        assert_eq!(cred.credential_key.auth_provider_type, TEST_AUTH_PROVIDER);
        assert_eq!(cred.credential_key.user_profile_id, TEST_ID);
        assert_eq!(cred.refresh_token, TEST_REFRESH_TOKEN);
        assert_eq!(cred.private_key, None);
    }

    #[test]
    fn test_new_valid_credential_with_private_key() {
        let cred = CredentialValue::new(
            TEST_AUTH_PROVIDER.to_string(),
            TEST_ID.to_string(),
            TEST_REFRESH_TOKEN.to_string(),
            Some(TEST_PRIVATE_KEY.to_vec()),
        )
        .unwrap();
        assert_eq!(cred.credential_key.auth_provider_type, TEST_AUTH_PROVIDER);
        assert_eq!(cred.credential_key.user_profile_id, TEST_ID);
        assert_eq!(cred.refresh_token, TEST_REFRESH_TOKEN);
        assert_eq!(cred.private_key, Some(TEST_PRIVATE_KEY.to_vec()));
    }

    #[test]
    fn test_new_invalid_credential() {
        assert!(CredentialValue::new(
            "".to_string(),
            TEST_ID.to_string(),
            TEST_REFRESH_TOKEN.to_string(),
            None
        )
        .is_err());
        assert!(CredentialValue::new(
            TEST_AUTH_PROVIDER.to_string(),
            "".to_string(),
            TEST_REFRESH_TOKEN.to_string(),
            None
        )
        .is_err());
        assert!(CredentialValue::new(
            TEST_AUTH_PROVIDER.to_string(),
            TEST_ID.to_string(),
            "".to_string(),
            None
        )
        .is_err());
    }
}
