Skip to content

Commit

Permalink
Version 10 (#4)
Browse files Browse the repository at this point in the history
* New config format and multi-profile support
* Clippy fixes
  • Loading branch information
jonathanmorley authored Mar 16, 2018
1 parent fcdc5d0 commit b76030e
Show file tree
Hide file tree
Showing 9 changed files with 644 additions and 246 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = false
32 changes: 19 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
[package]
name = "oktaws"
version = "0.9.3"
version = "0.10.0"
authors = ["Jonathan Morley <[email protected]>"]
description = "Generates temporary AWS credentials with Okta."

[dependencies]
reqwest = "0.8.1"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_str = "0.1.0"
toml = "0.4.5"
rusoto_core = "0.30.0"
rusoto_sts = "0.30.0"
rusoto_credential = "0.9.2"
scraper = "0.4.0"
base64 = "0.8.0"
quick-xml = "0.10.0"
rust-ini = "0.9"
structopt = "0.1.0"
structopt-derive = "0.1.0"
rpassword = "2.0.0"
text_io = "0.1.6"
base64 = "0.9.0"
rust-ini = "0.10.3"
structopt = "0.2.5"
structopt-derive = "0.2.5"
failure = "0.1.1"
log = "0.3.8"
pretty_env_logger = "0.1.1"
keyring = "0.5.1"
log = "0.4"
loggerv = "0.7.1"
keyring = "0.6"
username = "0.2.0"
dialoguer = "0.1.0"
sxd-document = "0.2.6"
sxd-xpath = "0.4.1"
kuchiki = "0.7.0"
regex = "0.2"

path_abs = "0.3.16"

[patch.crates-io]
keyring = { git = 'https://github.com/jonathanmorley/keyring-rs', branch = 'update-rpassword' }
html5ever = { git = 'https://github.com/servo/html5ever', rev = '3d5e24bbc3ebadf4e1bb9a4e25dc24c80fed1670' }
39 changes: 19 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# oktaws

This program authenticates with Okta, assumes a provided role, and pulls a temporary key with STS to then support the role assumption built into the aws cli.
This program authenticates with Okta, assumes a provided role, and pulls a temporary key with STS to support the role assumption built into the aws cli.

## Installation

Expand All @@ -17,64 +17,63 @@ curl -LSfs https://japaric.github.io/trust/install.sh | sh -s -- --git jonathanm

## Setup

First, create an `~/.oktaws/config` file with your Okta base URL, app URL and user ARN, like below:
First, create an `~/.oktaws/<OKTA ACCOUNT>.toml` file with the following information:

```
[aws_profile_name]
organization = mycompany
app_id = YOUR_APP/OKTA_MAGIC
role = arn:aws:iam::MY_ACCOUNT_ID:role/initial_role
```
username = '<USERNAME>'
role = '<DEFAULT ROLE>'
The `role` value above is the ARN of the Role you would like to log in as. This can be found in the Roles section of the IAM service of your account.
[profiles]
profile1 = '<OKTA APPLICATION NAME>'
profile2 = { application = '<OKTA APPLICATION NAME>', role = '<ROLE OVERRIDE>' }
```

You can find the other values above by going to your Identity Provider in the IAM service of your AWS account and downloading the metadata.
The metadata will contain some `<md:SingleSignOnService>` elements, where the `Location` attribute will look like https://mycompany.okta.com/app/YOUR_APP/OKTA_MAGIC/sso/saml"
The parts of this URL will correspond to the values above.
The `role` value above is the name (not ARN) of the role you would like to log in as. This can be found when logging into the AWS console through Okta.

Second, ensure that the `~/.aws/credentials` file does not contain important information under the `aws_profile_name` section, as they will be overwritten with temporary credentials. This file might look like the following:
Second, ensure that the `~/.aws/credentials` file does not contain important information under the `profile1` or `profile2` sections (or other profiles you define in `~/.oktaws/*.toml`), as they will be overwritten with temporary credentials. This file might look like the following:

```
[aws_profile_name]
[profile1]
aws_access_key_id = REDACTED
aws_secret_access_key = REDACTED
aws_session_token = REDACTED
```

The `~/.aws/config` file is read for information, but not modified. It should look similar to the following to link the profile section with the temporary credentials.
See [Assuming a Role](https://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html) for information on configuring the AWS CLI to assume a role.

```
[default]
output = json
region = us-east-1
[profile aws_profile_name]
[profile profile1]
role_arn = arn:aws:iam::MY_ACCOUNT_ID:role/final_role
source_profile = aws_profile_name
source_profile = profile1
```

With those things set up, you should be able to run `oktaws aws_profile_name`
With those things set up, you can run `oktaws profile1` to generate keys for a single profile, or `oktaws` to generate keys for all profiles.

## Usage

```sh
$ oktaws [AWS profile]
$ aws [command]
$ aws --profile [AWS profile] [command]
```

for example

```sh
$ oktaws production
$ aws ec2 describe-instances
$ aws --profile production ec2 describe-instances
```

## Debugging

Login didn't work? Launch this program with `DEBUG=oktaws*` in your environment for more debugging info:
Login didn't work? Use the `-v` flag to emit more verbose logs. Add more `-v`s for increased verbosity:

```sh
$ RUST_LOG=oktaws=debug oktaws production
$ oktaws production -vv
```

## Contributors
Expand Down
99 changes: 46 additions & 53 deletions src/aws.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
use base64::decode;
use failure::Error;
use ini::Ini;
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use rusoto_core::{default_tls_client, Region};
use rusoto_core;
use rusoto_core::Region;
use rusoto_sts::{AssumeRoleWithSAMLRequest, AssumeRoleWithSAMLResponse, Credentials, Sts,
StsClient};
use rusoto_credential::StaticProvider;

use std::env;
use std::str;
use std::collections::HashMap;
use std::str::FromStr;

#[derive(Serialize, Deserialize)]
struct AwsCredentialStore {
Expand All @@ -19,70 +17,64 @@ struct AwsCredentialStore {
aws_session_token: String,
}

pub fn find_saml_attributes(saml_assertion: &str) -> Result<HashMap<String, String>, Error> {
let decoded_saml = String::from_utf8(decode(&saml_assertion)?)?;
#[derive(Debug)]
pub struct Role {
pub provider_arn: String,
pub role_arn: String,
}

let mut reader = Reader::from_str(&decoded_saml);
reader.trim_text(true);
impl FromStr for Role {
type Err = Error;

let mut values = HashMap::new();
let mut buf = Vec::new();
let mut in_attribute_value = false;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let splitted: Vec<&str> = s.split(',').collect();

let attribute_value_name = b"saml2:AttributeValue";
match splitted.len() {
0 | 1 => bail!("Not enough elements in {}", s),
2 => Ok(Role {
provider_arn: String::from(splitted[0]),
role_arn: String::from(splitted[1]),
}),
_ => bail!("Too many elements in {}", s),
}
}
}

loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref e)) => {
if e.name() == attribute_value_name {
in_attribute_value = true;
}
}
Ok(Event::End(ref e)) => {
if e.name() == attribute_value_name {
in_attribute_value = false;
}
}
Ok(Event::Text(e)) => {
if in_attribute_value {
let value = e.unescape_and_decode(&reader).unwrap();
let splitted: Vec<&str> = value.split(',').collect();
impl Role {
pub fn role_name(&self) -> Result<&str, Error> {
let splitted: Vec<&str> = self.role_arn.split('/').collect();

if splitted.len() == 2 {
values.insert(splitted[1].to_owned(), splitted[0].to_owned());
}
}
}
Ok(Event::Eof) => break, // exits the loop when reaching end of file
Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e),
_ => (), // There are several other `Event`s we do not consider here
match splitted.len() {
0 | 1 => bail!("Not enough elements in {}", self.role_arn),
2 => Ok(splitted[1]),
_ => bail!("Too many elements in {}", self.role_arn),
}

// if we don't keep a borrow elsewhere, we can clear the buffer to keep memory usage low
buf.clear();
}

Ok(values)
}

pub fn assume_role(
principal_arn: &str,
role_arn: &str,
saml_assertion: &str,
Role {
provider_arn,
role_arn,
}: Role,
saml_assertion: String,
) -> Result<AssumeRoleWithSAMLResponse, Error> {
let provider = StaticProvider::new_minimal(String::from(""), String::from(""));

let req = AssumeRoleWithSAMLRequest {
duration_seconds: None,
policy: None,
principal_arn: String::from(principal_arn),
role_arn: String::from(role_arn),
saml_assertion: String::from(saml_assertion),
principal_arn: provider_arn,
role_arn,
saml_assertion,
};

let client = StsClient::new(default_tls_client()?, provider, Region::UsEast1);
let provider = StaticProvider::new_minimal(String::from(""), String::from(""));
let client = StsClient::new(
rusoto_core::default_tls_client()?,
provider,
Region::UsEast1,
);

Ok(client.assume_role_with_saml(&req)?)
client.assume_role_with_saml(&req).map_err(|e| e.into())
}

pub fn set_credentials(profile: &str, credentials: &Credentials) -> Result<(), Error> {
Expand All @@ -99,5 +91,6 @@ pub fn set_credentials(profile: &str, credentials: &Credentials) -> Result<(), E
)
.set("aws_session_token", credentials.session_token.to_owned());

Ok(conf.write_to_file(path)?)
info!("Saving AWS credentials to {}", path);
conf.write_to_file(path).map_err(|e| e.into())
}
Loading

0 comments on commit b76030e

Please sign in to comment.