Skip to content

Commit

Permalink
Merge pull request #93 from tristanmorgan/account_no_decode
Browse files Browse the repository at this point in the history
Decode Account IDs from access keys.
  • Loading branch information
tristanmorgan committed Oct 25, 2023
2 parents 83bee43 + 9266f62 commit bb336f4
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 80 deletions.
31 changes: 18 additions & 13 deletions .github/workflows/ruby.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,30 @@
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby

name: Ruby

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]

branches: ["main"]
permissions:
contents: read

jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Run tests
run: bundle exec rake
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.10
bundler-cache: true
- name: Run filemode
run: bundle exec rake filemode
- name: Run Rubocop
run: bundle exec rake rubocop
- name: Run Rspec
run: bundle exec rake spec
- name: Run Ronn Doc
run: bundle exec rake ronn
- name: Run Yard Doc
run: bundle exec rake yard
1 change: 1 addition & 0 deletions i18n/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ en:
add_role_desc: Adds a ROLE to the keyring
awskeyring_desc: Autocompletion for bourne shells
console_desc: Open the AWS Console for the ACCOUNT
decode_desc: Decode an account id from a KEY
default_desc: Run default help or initialise if needed.
env_desc: Outputs bourne shell environment exports for an ACCOUNT
exec_desc: Execute a COMMAND with the environment set for an ACCOUNT
Expand Down
14 changes: 14 additions & 0 deletions lib/awskeyring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'json'
require 'keychain'
require 'awskeyring/validate'
require 'awskeyring/awsapi'

# Awskeyring Module,
# gives you an interface to access keychains and items.
Expand Down Expand Up @@ -195,6 +196,19 @@ def self.list_account_names
(items + tokens).uniq.sort
end

# Return a list account item names plus account ids
def self.list_account_names_plus # rubocop:disable Metrics/AbcSize
list_items.concat(list_tokens).map do |elem|
account_id = Awskeyring::Awsapi.get_account_id(key: elem.attributes[:account])
account_name = if elem.attributes[:label].start_with?(ACCOUNT_PREFIX)
elem.attributes[:label][(ACCOUNT_PREFIX.length)..]
else
elem.attributes[:label][(SESSION_KEY_PREFIX.length)..]
end
"#{account_name}\t#{account_id}"
end.uniq.sort
end

# Return a list role item names
def self.list_role_names
list_roles.map { |elem| elem.attributes[:label][(ROLE_PREFIX.length)..] }.sort
Expand Down
33 changes: 30 additions & 3 deletions lib/awskeyring/awsapi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module Awsapi # rubocop:disable Metrics/ModuleLength
# AWS Env vars
AWS_ENV_VARS = %w[
AWS_ACCOUNT_NAME
AWS_ACCOUNT_ID
AWS_ACCESS_KEY_ID
AWS_ACCESS_KEY
AWS_CREDENTIAL_EXPIRATION
Expand Down Expand Up @@ -123,14 +124,16 @@ def self.get_cred_json(key:, secret:, token:, expiry:)
# [String] secret The aws_secret_access_key
# [String] token The aws_session_token
# @return [Hash] env_var hash
def self.get_env_array(params = {})
def self.get_env_array(params = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
env_var = {}
env_var['AWS_DEFAULT_REGION'] = 'us-east-1' unless region

params[:expiration] = Time.at(params[:expiry]).iso8601 unless params[:expiry].nil?
params[:account_name] = params.delete(:account)
params[:account_id] = get_account_id(key: params[:key]) unless params[:key].nil?

params.each_key do |param_name|
AWS_ENV_VARS.each do |var_name|
AWS_ENV_VARS.each do |var_name|
params.each_key do |param_name|
if var_name.include?(param_name.to_s.upcase) && !params[param_name].nil?
env_var[var_name] = params[param_name]
end
Expand Down Expand Up @@ -229,6 +232,30 @@ def self.region
region || Aws.shared_config.region(profile: 'default')
end

# Get the account number from an access key
#
# @param [String] key The aws_access_key_id
# @return [String] Account number
def self.get_account_id(key:)
padded_no = key[3..12]
mask = (2 << 39) - 1
decimal = (decode(padded_no) >> 4) & mask
decimal.to_s.rjust(12, '0')
end

# base32 decode function
# returns 0 on failure
private_class_method def self.decode(str)
aws_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
bytes = str.bytes
bytes.inject do |m, o|
i = aws_table.index(o.chr)
return 0 if i.nil?

(m << 5) + i
end
end

# Rotates the AWS access keys
#
# @param [String] key The aws_access_key_id
Expand Down
19 changes: 17 additions & 2 deletions lib/awskeyring_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,18 @@ def initialise
end

desc 'list', I18n.t('list_desc')
method_option :detail, type: :boolean, aliases: '-d', desc: I18n.t('method_option.detail'), default: false
# list the accounts
def list
if Awskeyring.list_account_names.empty?
warn I18n.t('message.missing_account', bin: File.basename($PROGRAM_NAME))
exit 1
end
puts Awskeyring.list_account_names.join("\n")
if options[:detail]
puts Awskeyring.list_account_names_plus.join("\n")
else
puts Awskeyring.list_account_names.join("\n")
end
end

desc 'list-role', I18n.t('list_role_desc')
Expand Down Expand Up @@ -426,6 +431,16 @@ def console(account = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLeng
end
end

desc 'decode KEY', I18n.t('decode_desc'), hide: true
# decode account numbers
def decode(key = nil)
key = ask_check(
existing: key, message: I18n.t('message.key'), validator: Awskeyring::Validate.method(:access_key)
)

puts Awskeyring::Awsapi.get_account_id(key: key)
end

desc "#{File.basename($PROGRAM_NAME)} CURR PREV", I18n.t('awskeyring_desc'), hide: true
map File.basename($PROGRAM_NAME) => :autocomplete
# autocomplete
Expand Down Expand Up @@ -522,7 +537,7 @@ def fetch_auto_resp(comp_type, sub_cmd)
# list command names
def list_commands
commands = self.class.all_commands.keys.map { |elem| elem.tr('_', '-') }
commands.reject! { |elem| %w[autocomplete default].include?(elem) }
commands.reject! { |elem| %w[autocomplete default decode].include?(elem) }
end

# list flags for a command
Expand Down
5 changes: 4 additions & 1 deletion man/awskeyring.5
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
.TH "AWSKEYRING" "5" "September 2023" "" ""
.TH "AWSKEYRING" "5" "October 2023" "" ""
.
.SH "NAME"
\fBAwskeyring\fR \- is a small tool to manage AWS account keys in the macOS Keychain
Expand Down Expand Up @@ -159,6 +159,9 @@ list:
.IP
Prints a list of accounts in the keyring
.
.IP
\-d, \-\-detail, \-\-no\-detail: Show more detail\.
.
.TP
list\-role:
.
Expand Down
2 changes: 2 additions & 0 deletions man/awskeyring.5.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ The commands are as follows:

Prints a list of accounts in the keyring

-d, --detail, --no-detail: Show more detail.

* list-role:

Prints a list of roles in the keyring<br>
Expand Down
41 changes: 27 additions & 14 deletions spec/lib/awskeyring/awsapi_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
instance_double(
Aws::STS::Types::AssumeRoleResponse,
assumed_role_user: {
arn: 'arn:aws:sts::123456789012:assumed-role/demo/Bob',
arn: 'arn:aws:sts::747118721026:assumed-role/demo/Bob',
assumed_role_id: 'ARO123EXAMPLE123:Bob'
},
credentials: {
Expand All @@ -62,7 +62,7 @@
instance_double(
Aws::STS::Types::AssumeRoleResponse,
assumed_role_user: {
arn: 'arn:aws:sts::123456789012:assumed-role/demo/Bob',
arn: 'arn:aws:sts::747118721026:assumed-role/demo/Bob',
assumed_role_id: 'ARO123EXAMPLE123:Bob'
},
credentials: {
Expand Down Expand Up @@ -217,7 +217,7 @@
end

context 'when credentials are verified' do
let(:key) { 'AKIA1234567890ABCDEF' }
let(:key) { 'AKIA234567ABCDEFGHIJ' }
let(:secret) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' }
let(:token) { 'AQoDYXdzEPT//////////wEXAMPLEtc764assume_roleDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMi' }

Expand All @@ -227,9 +227,9 @@
allow(described_class).to receive(:region).and_return(nil)
allow(Aws::STS::Client).to receive(:new).and_return(sts_client)
allow(sts_client).to receive(:get_caller_identity).and_return(
account: '123456789012',
arn: 'arn:aws:iam::123456789012:user/Alice',
user_id: 'AKIAI44QH8DHBEXAMPLE'
account: '747118721026',
arn: 'arn:aws:iam::747118721026:user/Alice',
user_id: 'AKIA234567ABCDEFGHIJ'
)
end

Expand All @@ -240,11 +240,19 @@
it 'calls get_caller_identity with a token' do
expect(awsapi.verify_cred(key: key, secret: secret, token: token)).to be(true)
end

it 'calls get_account_id with a valid token' do
expect(awsapi.get_account_id(key: key)).to eq('747118721026')
end

it 'calls get_account_id returning a short account number' do
expect(awsapi.get_account_id(key: 'AKIAQAAA67ABCDEFGHIJ')).to eq('000002029570')
end
end

context 'when keys are roated' do
let(:account) { 'test' }
let(:key) { 'AKIA1234567890ABCDEF' }
let(:key) { 'AKIA234567ABCDEFGHIJ' }
let(:secret) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' }
let(:new_key) { 'AKIAIOSFODNN7EXAMPLE' }
let(:new_secret) { 'wJalrXUtnFEMI/K7MDENG/bPxRiCYzEXAMPLEKEY' }
Expand All @@ -258,7 +266,7 @@
allow(iam_client).to receive_messages(
list_access_keys: { access_key_metadata: [
{
access_key_id: 'AKIATESTTEST',
access_key_id: 'AKIA234567ABCDEFGHIJ',
create_date: Time.parse('2016-12-01T22:19:58Z'),
status: 'Active',
user_name: 'Alice'
Expand Down Expand Up @@ -293,7 +301,7 @@

context 'when key rotation fails' do
let(:account) { 'test' }
let(:key) { 'AKIA1234567890ABCDEF' }
let(:key) { 'AKIA234567ABCDEFGHIJ' }
let(:secret) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' }
let(:key_message) { '# You have two access keys for account test' }
let(:iam_client) { instance_double(Aws::IAM::Client) }
Expand Down Expand Up @@ -333,7 +341,7 @@

context 'when key rotation fails to delete' do
let(:account) { 'test' }
let(:key) { 'AKIA1234567890ABCDEF' }
let(:key) { 'AKIA234567ABCDEFGHIJ' }
let(:secret) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' }
let(:new_key) { 'AKIAIOSFODNN7EXAMPLE' }
let(:new_secret) { 'wJalrXUtnFEMI/K7MDENG/bPxRiCYzEXAMPLEKEY' }
Expand Down Expand Up @@ -402,13 +410,14 @@
expect(awsapi.get_env_array(
account: 'test',
expiry: 1_489_305_329,
key: 'ASIAIOSFODNN7EXAMPLE',
key: 'ASIA234567ABCDEFGHIJ',
secret: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY',
token: role_token
)).to eq(
'AWS_ACCESS_KEY' => 'ASIAIOSFODNN7EXAMPLE',
'AWS_ACCESS_KEY_ID' => 'ASIAIOSFODNN7EXAMPLE',
'AWS_ACCESS_KEY' => 'ASIA234567ABCDEFGHIJ',
'AWS_ACCESS_KEY_ID' => 'ASIA234567ABCDEFGHIJ',
'AWS_ACCOUNT_NAME' => 'test',
'AWS_ACCOUNT_ID' => '747118721026',
'AWS_CREDENTIAL_EXPIRATION' => Time.at(1_489_305_329).iso8601,
'AWS_DEFAULT_REGION' => 'us-east-1',
'AWS_SECRET_ACCESS_KEY' => 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY',
Expand All @@ -420,7 +429,7 @@
end

context 'when existing creds are loaded from a file' do
let(:key) { 'AKIA1234567890ABCDEF' }
let(:key) { 'AKIA234567ABCDEFGHIJ' }
let(:secret) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' }
let(:token) { 'AQoDYXdzEPT//////////wEXAMPLEtc764assume_roleDOk4x4HIZ8j4FZTwdQWLWsKWHGBuFqwAeMi' }
let(:account_key) { 'AKIAIOSFODNN7EXAMPLE' }
Expand Down Expand Up @@ -523,5 +532,9 @@
)
end.to raise_error(SystemExit).and output(/The security token included in the request is invalid/).to_stderr
end

it 'calls get_account_id with an invalid token' do
expect(awsapi.get_account_id(key: key)).to eq('000000000000')
end
end
end
7 changes: 4 additions & 3 deletions spec/lib/awskeyring_command_exceptional_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
allow(Awskeyring::Awsapi).to receive(:region).and_return(nil)
allow(Awskeyring).to receive_messages(
get_valid_creds: { account: 'test',
key: 'ASIATESTTEST',
key: 'AKIA234567ABCDEFGHIJ',
secret: 'bigerlongbase64',
token: nil,
updated: Time.parse('2011-08-01T22:20:01Z') },
Expand Down Expand Up @@ -89,8 +89,9 @@
expect { described_class.start(%w[env test --force]) }
.to output(%(export AWS_DEFAULT_REGION="us-east-1"
export AWS_ACCOUNT_NAME="test"
export AWS_ACCESS_KEY_ID="ASIATESTTEST"
export AWS_ACCESS_KEY="ASIATESTTEST"
export AWS_ACCOUNT_ID="747118721026"
export AWS_ACCESS_KEY_ID="AKIA234567ABCDEFGHIJ"
export AWS_ACCESS_KEY="AKIA234567ABCDEFGHIJ"
export AWS_SECRET_ACCESS_KEY="bigerlongbase64"
export AWS_SECRET_KEY="bigerlongbase64"
unset AWS_CREDENTIAL_EXPIRATION
Expand Down
Loading

0 comments on commit bb336f4

Please sign in to comment.