Skip to content

Instantly share code, notes, and snippets.

@lukaszkorecki
Last active Nov 11, 2022
Embed
What would you like to do?
(ns bucket.client.credentials.ecs
"ECS credentials are full automatic - because the underlying Fargate (or EC2)
instance exposes an API for fetching credentials.
While cognitect.aws.api has a version for ECS + EC2, we had to create our own for
Fargate + ECS as the API is slightly different (for some reason): https://github.com/cognitect-labs/aws-api/blob/c4ca969975270ebbb5de18c7ad25b6fd391bf061/src/cognitect/aws/credentials.clj#L280
Basically, we make a request to an endpoint based on:
- metadata uri
- credentials relative uri
And parse it out"
(:require
[cheshire.core :as json]
[clojure.tools.logging :as log]
[cognitect.aws.credentials :as aws.credentials])
(:import
(java.net
URI)))
(defn fetch-credentials [env]
(let [uri (URI. (get env "ECS_CONTAINER_METADATA_URI"))
port (let [p (.getPort uri)]
(if (pos? p)
(str ":" p)
""))
host (.getHost uri)
scheme (.getScheme uri)
path (get env "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
creds (-> (format "%s://%s%s%s" scheme host port path)
slurp
(json/parse-string true))]
{:aws/access-key-id (:AccessKeyId creds)
:aws/secret-access-key (:SecretAccessKey creds)
:aws/session-token (:Token creds)
:cognitect.aws.credentials/ttl (aws.credentials/calculate-ttl creds)}))
(defn provider
"Creates a credential provider which periodically refreshes credentials
from the provided metadata endpoint in ECS"
[env]
(aws.credentials/cached-credentials-with-auto-refresh
(reify aws.credentials/CredentialsProvider
(fetch [_]
(try
(fetch-credentials env)
(catch Exception e
(log/errorf e "failed to fetch credentials %s" env)))))))
(ns bucket.client.credentials.profile
"Implements AWS profile authentication, but with access tokens sourced
from the role bound to the federated SSO user.
This reimplements the approach found in aws-wrap
https://github.com/linaro-its/aws2-wrap
with a couple of differences:
- we assume existence of SSO profiles in ~/.aws/config (or mounted dir in a container)
- we use the SSO credentials API directly - rather than shelling out to the AWS CLI (as it might not be
present in all contexts)
- we ignore assumed roles mechanism - we don't need it"
(:require
[bucket.utils.time :as time]
[cheshire.core :as json]
[clojure.java.io :as io]
[clojure.tools.logging :as log]
[cognitect.aws.config :as aws.config]
[cognitect.aws.credentials :as aws.credentials])
(:import
(java.io
File)
(java.net
HttpURLConnection
URL)
(java.time
Instant)
(java.util
Date)))
(defn read-aws-config
"Read the profile info from the main aws cli/sdk configuration file."
[path profile]
(let [f (io/file path)
_ (log/infof "reading profile=%s" f)
profiles (aws.config/parse f)
profile-info (get profiles profile)]
{:profile profile
:start-url (get profile-info "sso_start_url" nil)
:region (get profile-info "sso_region" nil)
:account-id (get profile-info "sso_account_id" nil)
:role-name (get profile-info "sso_role_name" nil)}))
(defn get-token-from-sso-cache
"Traverse all cached credential files found in ~/.aws/sso/cache
parse them (they're json) and return token if found in any of these files.
There's always one with a valid token, but the name is auto-generated and it also might expire."
[sso-cache-path]
(log/infof "reading-sso-cache %s" sso-cache-path)
(let [auth-data (->> sso-cache-path
io/file
file-seq
(filter (memfn ^File isFile))
((fn [x]
(map #(println (.getName %)) x)
x))
(filter #(re-find #"json$" (.getName %)))
(map slurp)
(map #(json/parse-string % true))
(filter :accessToken)
first)
{:keys [accessToken expiresAt]} auth-data]
(when (and accessToken expiresAt
;; expiresAt here IS NOT iso8601 but some date-time str
;; with UTC appened (wtf) - so we have to convert it to a zoned date time
;; note that this is different expiration time and AWS creds expiration time:
;; - local access token as generated by SSO lasts 24 hrs
;; - aws creds fetched below expire within an hour usually
;; The former requires manual refresh via `aws sso login`
;; the latter will be refreshed by Cognitect's credentials machinery
(not (time/expired? (time/str->date-time expiresAt))))
accessToken)))
(defn make-request [{:keys [token portal-url]}]
(let [url (URL. portal-url)
conn (.openConnection url)]
(.setRequestProperty conn "x-amz-sso_bearer_token" token)
(.setRequestMethod ^HttpURLConnection conn "GET")
(.setDoOutput conn true)
(.connect conn)
(with-open [out (.getInputStream conn)]
(-> (io/input-stream out)
slurp
(json/parse-string true)))))
(defn get-credentials-from-sso-api
"Implements call to https://docs.aws.amazon.com/singlesignon/latest/PortalAPIReference/API_GetRoleCredentials.html"
[{:keys [sso token]}]
(let [url (format
"https://portal.sso.%s.amazonaws.com:443/federation/credentials?account_id=%s&role_name=%s"
(:region sso)
(:account-id sso)
(:role-name sso))
_ (log/infof "requesting-token region=%s account-id=%s role=%s" (:region sso) (:account-id sso) (:role-name sso))
body (make-request {:token token :portal-url url})]
(:roleCredentials body)))
(defn fetch-credentials-from-sso-profile
"Fetches temporary AWS credentials for given config profile:
- read sso info for given profile from the aws config
- parse out pre-authd SSO access token from the SSO cache
- make a request to the AWS API to get the credentials"
([]
(fetch-credentials-from-sso-profile (System/getenv "AWS_PROFILE")))
([profile-name]
(fetch-credentials-from-sso-profile profile-name (or
(System/getenv "AWS_CONFIG_HOME")
(str (System/getenv "HOME") "/.aws"))))
([profile-name aws-root]
(let [sso-config (read-aws-config (str aws-root "/config") profile-name)
access-token (get-token-from-sso-cache (str aws-root "/sso/cache"))]
(when-not access-token
(throw
(ex-info
(str "AWS auth missing or expired, please log via SSO: aws sso login --profile="
profile-name) {})))
(get-credentials-from-sso-api {:sso sso-config
:token access-token}))))
(defn fetch-from-profile [profile]
(let [{:keys [accessKeyId secretAccessKey
sessionToken expiration]} (fetch-credentials-from-sso-profile profile)
;; another incompatibility with other AWS APIs:
;; rather than sending iso8601 date, we get a unix timestamp
;; XXX: this is a bug in cognitect/aws-api:
;; internally calculate-ttl works with instants, so we wouldn't need the
;; Date/from call - but as it happens `inst?` predicate function works with both Date's a
;; and Instants... Waiting for the fix to be merged.4
expiration-inst (Date/from (Instant/ofEpochMilli expiration))]
{:aws/access-key-id accessKeyId
:aws/secret-access-key secretAccessKey
:aws/session-token sessionToken
:cognitect.aws.credentials/ttl (aws.credentials/calculate-ttl {:Expiration expiration-inst})}))
(defn provider
"Creates a credential provider which periodically refreshes credentials
by using the SSO profile"
[profile]
(aws.credentials/cached-credentials-with-auto-refresh
(reify aws.credentials/CredentialsProvider
(fetch [_]
(try
(fetch-from-profile profile)
(catch Exception e
(log/errorf e "failed to refresh profile %s" profile)))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment