Unverified Commit 61dadaeb authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Credentials Provider Initial Implementation (#179)

parent ec0b06d1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
target/
Cargo.lock
+9 −0
Original line number Diff line number Diff line
[package]
name = "auth"
version = "0.1.0"
authors = ["Russell Cohen <rcoh@amazon.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
+97 −0
Original line number Diff line number Diff line
pub mod provider;

use std::time::SystemTime;
use std::error::Error;
use std::fmt::{Display, Formatter, Debug};
use std::fmt;

/// AWS SDK Credentials
///
/// An opaque struct representing credentials that may be used in an AWS SDK, modeled on
/// the [CRT credentials implementation](https://github.com/awslabs/aws-c-auth/blob/main/source/credentials.c).
///
/// Future design note: It may be desirable to make Credentials cheap to clone because they are cloned frequently.
#[derive(Clone)]
pub struct Credentials {
    access_key_id: String,
    secret_access_key: String,
    session_token: Option<String>,

    /// Credential Expiry
    ///
    /// A timepoint at which the credentials should no longer
    /// be used because they have expired. The primary purpose of this value is to allow
    /// credentials to communicate to the caching provider when they need to be refreshed.
    ///
    /// If these credentials never expire, this value will be set to `None`
    expires_after: Option<SystemTime>,

    provider_name: &'static str,
}

impl Debug for Credentials {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let mut creds = f.debug_struct("Credentials");
        creds.field("provider_name", &self.provider_name);
        creds.finish()
    }
}

const STATIC_CREDENTIALS: &'static str = "static";
impl Credentials {
    pub fn from_keys(
        access_key_id: impl Into<String>,
        secret_access_key: impl Into<String>,
        session_token: Option<String>,
    ) -> Self {
        Credentials {
            access_key_id: access_key_id.into(),
            secret_access_key: secret_access_key.into(),
            session_token,
            expires_after: None,

            provider_name: STATIC_CREDENTIALS
        }
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum CredentialsError {
    CredentialsNotLoaded,
    Unhandled(Box<dyn Error + Send + Sync + 'static>)
}

impl Display for CredentialsError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            CredentialsError::CredentialsNotLoaded => write!(f, "CredentialsNotLoaded"),
            CredentialsError::Unhandled(err) => write!(f, "{}", err)
        }
    }
}

impl Error for CredentialsError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            CredentialsError::Unhandled(e) => Some(e.as_ref() as _),
            _ => None
        }
    }
}

/// A Credentials Provider
///
/// This interface is intentionally NOT async. Credential providers should provide a separate
/// async method to drive refresh (eg. in a background task).
///
/// Pending future design iteration, an async credentials provider may be introduced.
pub trait ProvideCredentials: Send + Sync {
    fn credentials(&self) -> Result<Credentials, CredentialsError>;
}

impl ProvideCredentials for Credentials {
    fn credentials(&self) -> Result<Credentials, CredentialsError> {
        Ok(self.clone())
    }
}
+124 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

use crate::{ProvideCredentials, Credentials, CredentialsError};
use std::env::VarError;
use std::collections::HashMap;

/// Load Credentials from Environment Variables
pub struct EnvironmentVariableCredentialsProvider {
    env: Box<dyn Fn(&str) -> Result<String, VarError> + Send + Sync>
}

impl EnvironmentVariableCredentialsProvider {
    pub fn new() -> Self {
        EnvironmentVariableCredentialsProvider { env: Box::new(var) }
    }

    /// Create a EnvironmentVariable provider from a HashMap for testing
    fn for_map(env: HashMap<String, String>) -> Self {
        EnvironmentVariableCredentialsProvider {
            env: Box::new(move |key: &str| {
                env.get(key).ok_or(VarError::NotPresent).map(|k|k.to_string())
            })
        }
    }
}

fn var(key: &str) -> Result<String, VarError> {
    std::env::var(key)
}

const ENV_PROVIDER: &'static str = "EnvironmentVariable";

impl ProvideCredentials for EnvironmentVariableCredentialsProvider {
    fn credentials(&self) -> Result<Credentials, CredentialsError> {
        let access_key = (self.env)("AWS_ACCESS_KEY_ID").map_err(to_cred_error)?;
        let secret_key =
            (self.env)("AWS_SECRET_ACCESS_KEY").or_else(|_|(self.env)("SECRET_ACCESS_KEY")).map_err(to_cred_error)?;
        let session_token = (self.env)("AWS_SESSION_TOKEN").ok();
        Ok(Credentials {
            access_key_id: access_key,
            secret_access_key: secret_key,
            session_token,
            expires_after: None,
            provider_name: ENV_PROVIDER
        })
    }
}

fn to_cred_error(err: VarError) -> CredentialsError {
    match err {
        VarError::NotPresent => CredentialsError::CredentialsNotLoaded,
        e @ VarError::NotUnicode(_) => CredentialsError::Unhandled(Box::new(e))
    }
}

#[cfg(test)]
mod test {
    use crate::provider::EnvironmentVariableCredentialsProvider;
    use std::collections::HashMap;
    use crate::{ProvideCredentials, CredentialsError};

    #[test]
    fn valid_no_token() {
        let mut env = HashMap::new();
        env.insert("AWS_ACCESS_KEY_ID".to_owned(), "access".to_owned());
        env.insert("AWS_SECRET_ACCESS_KEY".to_owned(), "secret".to_owned());

        let provider = EnvironmentVariableCredentialsProvider::for_map(env);
        let creds = provider.credentials().expect("valid credentials");
        assert_eq!(creds.session_token, None);
        assert_eq!(creds.access_key_id, "access");
        assert_eq!(creds.secret_access_key, "secret");
    }

    #[test]
    fn valid_with_token() {
        let mut env = HashMap::new();
        env.insert("AWS_ACCESS_KEY_ID".to_owned(), "access".to_owned());
        env.insert("AWS_SECRET_ACCESS_KEY".to_owned(), "secret".to_owned());
        env.insert("AWS_SESSION_TOKEN".to_owned(), "token".to_owned());

        let provider = EnvironmentVariableCredentialsProvider::for_map(env);
        let creds = provider.credentials().expect("valid credentials");
        assert_eq!(creds.session_token.unwrap(), "token");
        assert_eq!(creds.access_key_id, "access");
        assert_eq!(creds.secret_access_key, "secret");
    }

    #[test]
    fn secret_key_fallback() {
        let mut env = HashMap::new();
        env.insert("AWS_ACCESS_KEY_ID".to_owned(), "access".to_owned());
        env.insert("SECRET_ACCESS_KEY".to_owned(), "secret".to_owned());
        env.insert("AWS_SESSION_TOKEN".to_owned(), "token".to_owned());

        let provider = EnvironmentVariableCredentialsProvider::for_map(env);
        let creds = provider.credentials().expect("valid credentials");
        assert_eq!(creds.session_token.unwrap(), "token");
        assert_eq!(creds.access_key_id, "access");
        assert_eq!(creds.secret_access_key, "secret");

    }

    #[test]
    fn missing() {
        let env = HashMap::new();
        let provider = EnvironmentVariableCredentialsProvider::for_map(env);
        let err = provider.credentials().expect_err("no credentials defined");
        match err {
            CredentialsError::Unhandled(_ ) => panic!("wrong error type"),
            _ => ()
        };
    }

    #[test]
    fn real_environment() {
        let provider = EnvironmentVariableCredentialsProvider::new();
        // we don't know what's in the env, just make sure it doesn't crash.
        let _ = provider.credentials();
    }
}
+19 −0
Original line number Diff line number Diff line
#!/bin/bash

#
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0.
#

set -e
for crate in "$(dirname "$0")"/*/
do
  if [ -d "$crate" ] && [ -f "$crate/Cargo.toml" ]; then
    echo "Testing $crate"
    (cd "$crate" && cargo fmt)
    (cd "$crate" && cargo fmt -- --check)
    (cd "$crate" && cargo clippy -- -D warnings)
    (cd "$crate" && cargo test)
    (cd "$crate" && cargo doc --no-deps)
  fi
done
Loading