Skip to content

Commit

Permalink
Credentials Provider Initial Implementation (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
rcoh authored Jan 28, 2021
1 parent ec0b06d commit 61dadae
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 1 deletion.
2 changes: 2 additions & 0 deletions aws/rust-runtime/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
9 changes: 9 additions & 0 deletions aws/rust-runtime/auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[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 changes: 97 additions & 0 deletions aws/rust-runtime/auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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 changes: 124 additions & 0 deletions aws/rust-runtime/auth/src/provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 changes: 19 additions & 0 deletions aws/rust-runtime/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/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
13 changes: 13 additions & 0 deletions aws/sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ val sdkOutputDir = buildDir.resolve("aws-sdk")
val awsServices = discoverServices()
// TODO: smithy-http should be removed
val runtimeModules = listOf("smithy-types", "smithy-http")
val awsModules = listOf("auth")

buildscript {
val smithyVersion: String by project
Expand Down Expand Up @@ -103,6 +104,18 @@ task("relocateServices") {
}
}

tasks.register<Copy>("relocateAwsRuntime") {
from("$rootDir/aws/rust-runtime")
awsModules.forEach {
include("$it/**")
}
exclude("**/target")
exclude("**/Cargo.lock")
// filter { line -> line.replace("../../rust-runtime/", "") }
into(sdkOutputDir)
outputs.upToDateWhen { false }
}

tasks.register<Copy>("relocateRuntime") {
from("$rootDir/rust-runtime") {
runtimeModules.forEach {
Expand Down
4 changes: 3 additions & 1 deletion rust-runtime/smithy-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ impl Error {
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
pub fn request_id(&self) -> Option<&str> { self.request_id.as_deref() }
pub fn request_id(&self) -> Option<&str> {
self.request_id.as_deref()
}
}

impl Display for Error {
Expand Down
1 change: 1 addition & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export SMITHY_TEST_WORKSPACE=~/.smithy-test-workspace
./gradlew ktlintFormat
./gradlew ktlint
rust-runtime/test.sh
aws/rust-runtime/test.sh

0 comments on commit 61dadae

Please sign in to comment.