Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ This section covers the credentials the CLI gives AWS tools, for restricted data

When the credentials expire, `source-coop creds` gets new ones with the session. Run `source-coop auth login` again only if this fails, for example because the refresh token expired or was revoked.

![`aws s3 ls` through `credential_process`, then `creds --format env`](docs/demos/aws.svg)
![`aws s3 ls` through `credential_process`, then with `creds --format env`](docs/demos/aws.svg)

### Output formats

Expand All @@ -266,12 +266,23 @@ When the credentials expire, `source-coop creds` gets new ones with the session.
source-coop creds
```

**env**: shell `export` statements. They set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` in your shell.
**env**: shell `export` statements. They set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` in your shell, and point S3 and STS at the data proxy (`--proxy-url`), so the AWS CLI and SDKs need no `--endpoint-url`:

```bash
eval $(source-coop creds --format env)
aws s3 ls s3://ACCOUNT/PRODUCT/
```

After the credentials, it prints:

```bash
export AWS_REGION=us-west-2
export AWS_ENDPOINT_URL_S3=https://data.source.coop
export AWS_ENDPOINT_URL_STS=https://data.source.coop/.sts
```

Only S3 and STS go to the proxy; other AWS services keep their usual endpoints. The region is there because some SDKs won't run without one: the proxy accepts any. The AWS CLI v2 (2.13 or later), boto3 (1.28 or later), s3fs, obstore and polars read `AWS_ENDPOINT_URL_S3`. GDAL and DuckDB don't, so give them the endpoint their own way.

**aws-credentials**: an INI profile for `~/.aws/credentials`. `--profile` sets the section name.

```bash
Expand Down
18 changes: 13 additions & 5 deletions docs/demos/aws.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
{
"duration": 26.271,
"duration": 34.428,
"steps": [
{
"time": 0.755,
"time": 0.761,
"label": "$ cat ~/.aws/config"
},
{
"time": 4.605,
"time": 4.589,
"label": "$ aws s3 ls s3://demo-org/demo-data/ --profile source-coop"
},
{
"time": 12.597,
"time": 12.642,
"label": "$ aws s3 ls s3://demo-org/demo-data/readings/ --profile source-coop --human-readable"
},
{
"time": 21.777,
"time": 21.718,
"label": "$ source-coop creds --format env"
},
{
"time": 26.212,
"label": "$ eval $(source-coop creds --format env)"
},
{
"time": 29.304,
"label": "$ aws s3 ls s3://demo-org/demo-data/"
}
],
"keys": []
Expand Down
2 changes: 1 addition & 1 deletion docs/demos/aws.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions docs/recorder/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ def aws(s):
s.run("cat ~/.aws/config"); s.prompt(); s.pause(2)
s.run(f"aws s3 ls s3://{ORG}/demo-data/ --profile source-coop"); s.prompt(); s.pause(1.5)
s.run(f"aws s3 ls s3://{ORG}/demo-data/readings/ --profile source-coop --human-readable"); s.prompt(); s.pause(1.5)
s.run("source-coop creds --format env")
s.run("source-coop creds --format env"); s.prompt(); s.pause(3)
s.run("eval $(source-coop creds --format env)"); s.prompt()
s.run(f"aws s3 ls s3://{ORG}/demo-data/") # no --profile or --endpoint-url


def prompts(s):
Expand Down Expand Up @@ -396,7 +398,7 @@ def service_account_usage(s):
"auth-status": ("`auth status`, then `--json`", auth_status),
"auth-token": ("`auth token`: the session's access token, for `curl`", auth_token),
"auth-logout": ("`auth logout --json`, then `auth status` and `creds` signed out", auth_logout),
"aws": ("`aws s3 ls` through `credential_process`, then `creds --format env`", aws),
"aws": ("`aws s3 ls` through `credential_process`, then with `creds --format env`", aws),
"prompts": ("Prompts: a refused ID asked again, a draft kept after a failure, then Esc", prompts),
"completion": ("Shell completion: TAB lists products and service accounts from the API", completion),
"account-list": ("`account list`, `view`, then `list --json`", account_list),
Expand Down
7 changes: 4 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ struct ServiceAccountArgs {
enum OutputFormat {
/// AWS credential_process JSON format
CredentialProcess,
/// Shell export statements
/// Shell export statements: the credentials, AWS_REGION, and the
/// --proxy-url as the S3 and STS endpoints
Env,
/// AWS credentials file (INI) profile
AwsCredentials,
Expand Down Expand Up @@ -950,7 +951,7 @@ async fn run_login(args: LoginArgs, verbose: bool) -> Result<(), String> {
eprintln!("Skipping credential cache (--no-cache)");
match args.output.format {
OutputFormat::CredentialProcess => output::print_credential_process(&creds),
OutputFormat::Env => output::print_env(&creds),
OutputFormat::Env => output::print_env(&creds, &args.env.proxy_url),
OutputFormat::AwsCredentials => {
output::print_aws_credentials(&creds, &args.output.profile)
}
Expand Down Expand Up @@ -1050,7 +1051,7 @@ async fn run_creds(args: CredsArgs, verbose: bool) -> Result<(), String> {

match args.output.format {
OutputFormat::CredentialProcess => output::print_credential_process(&entry.creds),
OutputFormat::Env => output::print_env(&entry.creds),
OutputFormat::Env => output::print_env(&entry.creds, &args.env.proxy_url),
OutputFormat::AwsCredentials => {
output::print_aws_credentials(&entry.creds, &args.output.profile)
}
Expand Down
56 changes: 51 additions & 5 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,33 @@ pub fn print_credential_process(creds: &Credentials) {
println!("{}", serde_json::to_string_pretty(&json).unwrap());
}

/// Print credentials as shell export statements.
pub fn print_env(creds: &Credentials) {
println!("export AWS_ACCESS_KEY_ID={}", creds.access_key_id);
println!("export AWS_SECRET_ACCESS_KEY={}", creds.secret_access_key);
println!("export AWS_SESSION_TOKEN={}", creds.session_token);
/// The exports that point a stock AWS SDK or the AWS CLI at the data proxy:
/// S3 and STS only (a global `AWS_ENDPOINT_URL` would send IAM and every
/// other service there too), and a region, which the Rust and Go SDKs won't
/// run without and the proxy ignores.
pub fn endpoint_env(proxy: &str) -> [String; 3] {
let proxy = proxy.trim_end_matches('/');
[
"export AWS_REGION=us-west-2".to_string(),
format!("export AWS_ENDPOINT_URL_S3={proxy}"),
format!("export AWS_ENDPOINT_URL_STS={proxy}/.sts"),
]
}

/// Credentials as shell export statements, with the proxy's endpoints.
fn env(creds: &Credentials, proxy: &str) -> String {
let mut lines = vec![
format!("export AWS_ACCESS_KEY_ID={}", creds.access_key_id),
format!("export AWS_SECRET_ACCESS_KEY={}", creds.secret_access_key),
format!("export AWS_SESSION_TOKEN={}", creds.session_token),
];
lines.extend(endpoint_env(proxy));
lines.join("\n")
}

/// Print credentials as shell export statements, with the proxy's endpoints.
pub fn print_env(creds: &Credentials, proxy: &str) {
println!("{}", env(creds, proxy));
}

/// Print credentials as an AWS credentials-file (INI) profile,
Expand All @@ -37,3 +59,27 @@ pub fn print_aws_credentials(creds: &Credentials, profile: &str) {
println!("aws_secret_access_key = {}", creds.secret_access_key);
println!("aws_session_token = {}", creds.session_token);
}

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

#[test]
fn env_exports_credentials_and_the_proxy_s_endpoints() {
let creds = Credentials {
access_key_id: "AKID".into(),
secret_access_key: "SECRET".into(),
session_token: "TOKEN".into(),
expiration: "2026-07-13T12:00:00Z".into(),
};
let want = "export AWS_ACCESS_KEY_ID=AKID\n\
export AWS_SECRET_ACCESS_KEY=SECRET\n\
export AWS_SESSION_TOKEN=TOKEN\n\
export AWS_REGION=us-west-2\n\
export AWS_ENDPOINT_URL_S3=https://data.source.coop\n\
export AWS_ENDPOINT_URL_STS=https://data.source.coop/.sts";
assert_eq!(env(&creds, "https://data.source.coop"), want);
// A trailing slash on --proxy-url doesn't double up before /.sts.
assert_eq!(env(&creds, "https://data.source.coop/"), want);
}
}
10 changes: 4 additions & 6 deletions src/service_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2759,14 +2759,12 @@ fn github_workflow(proxy: &str, id: &str, subject: &str) -> String {
/// What a stock AWS SDK or the AWS CLI needs to sign in with an API key saved
/// to a file: the web UI's `apiKeyEnvironment`.
fn api_key_environment(proxy: &str, id: &str) -> String {
[
"export AWS_REGION=us-west-2".to_string(),
format!("export AWS_ENDPOINT_URL_S3={proxy}"),
format!("export AWS_ENDPOINT_URL_STS={proxy}/.sts"),
let mut lines = crate::output::endpoint_env(proxy).to_vec();
lines.extend([
format!("export AWS_ROLE_ARN=arn:aws:iam::{id}:role/FullAccess"),
"export AWS_WEB_IDENTITY_TOKEN_FILE=/path/to/the/saved/key".into(),
]
.join("\n")
]);
lines.join("\n")
}

#[cfg(test)]
Expand Down
Loading