Skip to content

Commit

Permalink
Merge branch 'feature/gnupg'
Browse files Browse the repository at this point in the history
  • Loading branch information
naftulikay committed Jan 21, 2018
2 parents 195c881 + c2f409c commit 069411a
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 86 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/ansible/galaxy_roles/

/bin/
/build/
/develop-eggs/
Expand All @@ -7,8 +7,10 @@
/parts/
/src/*.egg-info

/.ansible/
/.eggs/
/.installed.cfg
/.python-version
/.vagrant

__pycache__
Expand Down
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ python:
- '2.7'
- '3.4'
- '3.5'
- '3.6'

cache:
pip: true
Expand All @@ -17,7 +18,7 @@ before_install:
- pip install -r requirements.txt

install:
- buildout
- buildout -c buildout-travis.cfg

script: bin/test

Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@ usage: aws-env [-h] [-n] [-l] [profile]
Extract AWS credentials for a given profile as environment variables.
positional arguments:
profile The profile in ~/.aws/credentials to extract credentials
for. Defaults to 'default'.
profile The profile in ~/.aws/credentials or ~/.aws/credentials.d/
to extract credentials for. Defaults to 'default'.
optional arguments:
-h, --help show this help message and exit
-n, --no-export Do not use export on the variables.
-l, --ls List available profiles.
```

`aws-env` looks first at `~/.aws/credentials` and then at all files in `~/.aws/credentials.d/` if found and merges all
of them together in a dictionary of profiles. `~/.aws/credentials` is loaded first and everything loaded from
`~/.aws/credentials.d/` are loaded in alphabetically sorted order and merged in. (See
[Encrypted Credential Files](#Encrypted Credential Files) for instructions on using encrypted credential files.)

If you have a profile named `brangus`, you can extract environment variables like so:

```shell
Expand All @@ -43,6 +48,25 @@ This will cause your shell to execute the output of `aws-env`, exporting these e
> you should be able to use this, but as always, _read the source code_ and check it before you blindly pipe code into
> your shell session.
### Encrypted Credential Files

`aws-env` now supports encrypted credential files! Stash files ending in `.asc`, `.gpg`, or `.pgp` in
`~/.aws/credentials.d/` and `aws-env` will attempt to decrypt these files using GnuPG. `gpg2` is preferred but `gpg`
will be used as a backup option.

`aws-env` will decrypt files directly into memory. File format should be the same as `~/.aws/credentials`. Here's a
sample `tree` output detailing the directory layout:

```
/home/naftuli/.aws
├── config
├── credentials
└── credentials.d
└── naftulikay.asc
1 directory, 3 files
```

## Installation

There already exists an `awsenv` package on PyPI, so this is not published to PyPI. I have a personal frustration with
Expand Down
14 changes: 9 additions & 5 deletions Vagrantfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# -*- mode: ruby -*-
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
# vi: set ft=ruby :

require 'etc'
require 'shellwords'

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Every Vagrant virtual environment requires a box to build off of.
config.vm.box = "bento/centos-7.3"
config.vm.box = "bento/centos-7.4"
config.vm.hostname = "devel"

# Create a private network, which allows host-only access to the machine
Expand All @@ -17,16 +19,18 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

# Tweak the VMs configuration.
config.vm.provider "virtualbox" do |vb|
vb.cpus = Etc.nprocessors
vb.memory = 1024
vb.linked_clone = true
end

# Configure the VM using Ansible
config.vm.provision "ansible_local" do |ansible|
ansible.galaxy_role_file = "requirements.yml"
ansible.galaxy_roles_path = ".ansible/galaxy-roles"
ansible.provisioning_path = "/vagrant"
ansible.playbook = "vagrant.yml"
# allow passing ansible args from environment variable
ansible.raw_arguments = Shellwords::shellwords(ENV.fetch("ANSIBLE_ARGS", ""))

ansible.provisioning_path = "/vagrant/ansible/"
ansible.playbook = "playbook.yml"
end
end
6 changes: 6 additions & 0 deletions ansible.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[defaults]
retry_files_enabled = false
roles_path = roles:.ansible/galaxy-roles

[ssh_connection]
ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
2 changes: 0 additions & 2 deletions ansible/ansible.cfg

This file was deleted.

31 changes: 0 additions & 31 deletions ansible/playbook.yml

This file was deleted.

20 changes: 20 additions & 0 deletions buildout-travis.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[buildout]
parts = python test
develop = .
eggs = awsenv
versions = versions

[versions]
# blank for now

[python]
recipe = zc.recipe.egg
interpreter = python
eggs = ${buildout:eggs}

[test]
recipe = pbp.recipe.noserunner
eggs = ${buildout:eggs}
pbp.recipe.noserunner
mock
script = test
3 changes: 3 additions & 0 deletions requirements.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
- name: vagrant-python-dev
src: naftulikay.vagrant-python-dev
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name = "awsenv",
version = "1.0.0",
version = "1.1.0",
packages = find_packages('src'),
package_dir = { '': 'src'},
author = "Naftuli Kay",
Expand Down
121 changes: 96 additions & 25 deletions src/awsenv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import argparse
import os
import subprocess
import sys

PYTHON_VERSION = sys.version_info[0]
Expand All @@ -16,7 +17,11 @@
else:
from configparser import ConfigParser

CREDENTIALS_PATH = "~/.aws/credentials"
CREDENTIALS_ROOT = os.path.expanduser("~/.aws")
CREDENTIALS_PATH = os.path.join(CREDENTIALS_ROOT, 'credentials')
CREDENTIALS_D_PATH = os.path.join(CREDENTIALS_ROOT, 'credentials.d')




def config_to_dict(config):
Expand All @@ -32,27 +37,97 @@ def config_to_dict(config):
return result


class GnuPG(object):
"""GnuPG utility class."""

__GNUPG_PATH = None

@classmethod
def path(cls):
"""Returns the absolute path to (in order) GnuPG 2 or GnuPG 1."""
if not cls.__GNUPG_PATH:
for trial in ['gpg2', 'gpg']:
p = subprocess.Popen(['which', trial], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, _ = p.communicate()

if p.returncode == 0:
cls.__GNUPG_PATH = stdout.strip().decode('utf-8')
return cls.__GNUPG_PATH

return cls.__GNUPG_PATH


class AWSCredentials(object):

@classmethod
def from_path(cls, path):
"""Load AWS credentials from a path into an AWSCredentials object."""
def find_files(cls):
"""Find credentials files and return a list."""
if not os.path.isdir(CREDENTIALS_D_PATH):
return [CREDENTIALS_PATH]

credentials_d_files = list(
filter(lambda p: os.path.isfile(p), sorted(map(lambda p: os.path.join(CREDENTIALS_D_PATH, p),
os.listdir(CREDENTIALS_D_PATH))))
)

return [CREDENTIALS_PATH] + credentials_d_files

@classmethod
def __load_encrypted(cls, p):
"""Loads an encrypted file into a dictionary of profile names to access and secret keys."""
decrypter = subprocess.Popen([GnuPG.path(), '-d', p], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, _ = decrypter.communicate()

if not decrypter.returncode == 0:
fail("ERROR: Unable to decrypt {}".format(p))

contents = stdout.strip().decode('utf-8')

config = ConfigParser()

try:
config.read_string(contents)
except:
fail("ERROR: Unable to parse {} as an INI-structured file.".format(p))

return config_to_dict(config)

@classmethod
def __load_plaintext(cls, p):
"""Loads a plaintext file into a dictionary of profile names to access and secret keys."""
config = ConfigParser()
config.read(path)

config = config_to_dict(config)
try:
config.read(p)
except:
fail("ERROR: Unable to load {} as an INI-structured file.".format(p))

profiles = {}
return config_to_dict(config)

for section in config.keys():
name = section
key_id = config.get(section).get('aws_access_key_id')
secret_key = config.get(section).get('aws_secret_access_key')
@classmethod
def load(cls):
"""Load all credentials into a dictionary."""
result = {}

for p in cls.find_files():
if p.lower().endswith('.asc') or p.lower().endswith('.gpg') or p.lower().endswith('.pgp'):
data = cls.__load_encrypted(p)
else:
data = cls.__load_plaintext(p)

data.update(result)
result = data

profile_map = {}

if key_id and len(key_id) > 0 and secret_key and len(secret_key) > 0:
profiles[name] = AWSProfile(name, key_id, secret_key)
for name in result.keys():
profile = result[name]
key_id, secret_key = profile.get('aws_access_key_id'), profile.get('aws_secret_access_key')

return AWSCredentials(**profiles)
if len(key_id or '') > 0 and len(secret_key or '') > 0:
profile_map[name] = AWSProfile(name=name, key_id=key_id, secret_key=secret_key)

return AWSCredentials(**profile_map)

def __init__(self, **kwargs):
self.profiles = kwargs
Expand Down Expand Up @@ -102,29 +177,24 @@ def main():
help="Do not use export on the variables.")
parser.add_argument('-l', '--ls', dest="list", action="store_true", help="List available profiles.")
parser.add_argument("profile", nargs="?", default="default",
help="The profile in ~/.aws/credentials to extract credentials for. Defaults to 'default'.")
help="The profile in ~/.aws/credentials or ~/.aws/credentials.d/ to extract credentials for. Defaults to 'default'.")
args = parser.parse_args()

config_file_path = os.path.expanduser(CREDENTIALS_PATH)

if not os.path.isfile(config_file_path):
fail("Unable to load credentials file from {}".format(config_file_path))
credentials = AWSCredentials.load()

credentials = AWSCredentials.from_path(config_file_path)
user_cred_path = CREDENTIALS_PATH.replace(os.environ.get('HOME'), '~')
user_cred_d_path = CREDENTIALS_D_PATH.replace(os.environ.get('HOME'), '~') + os.path.sep

if args.list:
if len(credentials.ls()) < 1:
sys.stderr.write("ERROR: {}\n".format("No profiles found."))
sys.stderr.flush()
return 1
fail("ERROR: No profiles found in {}, {}".format(user_cred_path, user_cred_d_path))

# just list the profiles and get out
sys.stdout.write("{}\n".format("\n".join(sorted(credentials.ls()))))
sys.stdout.flush()
print('\n'.join(sorted(credentials.ls())))
return 0

if args.profile not in credentials.ls():
fail("Profile {} does not exist in {}".format(args.profile, config_file_path))
fail("Profile '{}' not found in {}, {}/".format(args.profile, user_cred_path, user_cred_d_path))

profile = credentials.get(args.profile)

Expand All @@ -137,5 +207,6 @@ def fail(message):
sys.stderr.flush()
sys.exit(1)


if __name__ == "__main__":
main()
Loading

0 comments on commit 069411a

Please sign in to comment.