Skip to content

Commit

Permalink
Merge pull request #8 from Nr18/develop
Browse files Browse the repository at this point in the history
release: 0.2.0
  • Loading branch information
Joris Conijn committed Jul 21, 2022
2 parents 2d964dd + 545a89f commit d1ffac5
Show file tree
Hide file tree
Showing 30 changed files with 1,532 additions and 225 deletions.
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,62 @@ stored in the `~/.aws/credentials` file for re-use.
## Configuration

You will need to configure your roles and IAM User credentials in the same places as you are used to. So in your
`~/.aws/credentials` file you will need to have the following:
`~/.aws/credentials` file. To make this process as easy as possible you could use the following command:

```bash
aws-iam-login my-profile init
```

This command will fetch the ARN of the caller identity. Based on this identity we will determin the `username` and
`mfa_serial` of the IAM User. These will then be stored in the `~/.aws/credentials` file. For example:

```ini
[my-profile]
aws_access_key_id = XXXXXXX
aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXXXXXXXXX
mfa_serial = arn:aws:iam::111122223333:mfa/my-iam-user
username = my-iam-user
```

The only addition is the `mfa_serial` field.
The only addition is the `username` and `mfa_serial` fields.

### AWS Least privileged

Assuming you have an IAM User that is already configured you will need the following permissions to use `aws-iam-login`:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSessionTokeUsingMFA",
"Effect": "Allow",
"Action": [
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "true"
}
}
},
{
"Sid": "AllowAccessKeyRotation",
"Effect": "Allow",
"Action": [
"iam:ListAccessKeys",
"iam:CreateAccessKey",
"iam:UpdateAccessKey",
"iam:DeleteAccessKey"
],
"Resource": [
"arn:aws:iam::111122223333:user/${aws:username}"
]
}
]
}
```

## Usage

Expand All @@ -41,3 +87,19 @@ So the next time you use `AWS_PROFILE=my-role-1` the credentials will be present

Because you are already authenticated using MFA there is no need to provide an MFA token when you assume the role.
When you switch a lot between roles you really benefit from not having to type your MFA token each time you switch.

### Rotating your AccessKey and SecretAccessKey

It is advised to rotate your credentials regularly. `aws-iam-login` can help with that! By executing the following command:

```bash
aws-iam-login my-rofile rotate
```

This command will execute the following actions:

1. List all available keys for the user, when 1 key is active rotation is possible!
2. Create a new AccessKey and SecretAccessKey.
3. Use the newly created keys to deactivate the old keys.
4. Write the new keys to the `~/.aws/configuration` file.
5. Delete the old keys.
88 changes: 75 additions & 13 deletions aws_iam_login/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,89 @@
import click
import boto3
from .aws_config import AWSConfig
from .credentials import Credentials
from botocore.exceptions import ParamValidationError, ClientError
from click import Context

from aws_iam_login.actions.initialize_configuration import InitializeConfiguration
from aws_iam_login.observer import Observer
from aws_iam_login.temp_credentials import TempCredentials
from aws_iam_login.aws_config import AWSConfig
from aws_iam_login.actions.rotate_access_keys import RotateAccessKeys
from aws_iam_login.credentials import Credentials
from aws_iam_login.application_context import ApplicationContext, ApplicationMessages

@click.command()

@click.group(invoke_without_command=True)
@click.option("--debug/--no-debug")
@click.argument("profile")
def main(profile: str) -> None:
@click.pass_context
def main(ctx: Context, debug: bool, profile: str) -> None:
"""
aws-iam-login
"""
config = AWSConfig(profile)
session = boto3.Session(profile_name=profile)
ctx.obj = ApplicationContext(profile=profile, debug=debug)

if ctx.invoked_subcommand is None:
credentials(ctx=ctx.obj)


def credentials(ctx: ApplicationContext) -> None:
config = AWSConfig(ctx.profile)
session = boto3.Session(profile_name=ctx.profile)
client = session.client("sts")

response = client.get_session_token(
SerialNumber=config.mfa_serial,
TokenCode=input(f"Enter MFA code for {config.mfa_serial}: "),
)
try:
response = client.get_session_token(
SerialNumber=config.mfa_serial,
TokenCode=input(f"Enter MFA code for {config.mfa_serial}: "),
)

config.write(
f"{ctx.profile}-sts", TempCredentials(response.get("Credentials", {}))
)
click.echo(f"Login credentials stored in the {ctx.profile}-sts profile!")
except ClientError as exc:
click.echo(f"ClientError: {exc}")
exit(1)
except ParamValidationError as exc:
click.echo(f"ValidationError: {exc}")
exit(1)


@main.command()
@click.pass_obj
def init(ctx: ApplicationContext) -> None:
"""
Initialize your `.aws/configuration`
"""
click.echo(f"Looking up additional required data...")

action = InitializeConfiguration(profile=ctx.profile)
action.subscribe_subject(ctx.subject)

if not action.execute():
click.echo(f"Failed to initialize the {ctx.profile} profile")
exit(1)

click.echo(f"The {ctx.profile} profile has been successfully initialized!")


@main.command()
@click.pass_obj
def rotate(ctx: ApplicationContext) -> None:
"""
Rotate your IAM User credentials
"""
click.echo(f"Key rotation process in progress")

action = RotateAccessKeys(profile=ctx.profile)
action.subscribe_subject(ctx.subject)

if not action.execute():
click.echo(f"Failed to rotate the access keys for the {ctx.profile} profile")
exit(1)

config.write(f"{profile}-sts", Credentials(response.get("Credentials", {})))
click.echo(f"Login credentials stored in the {profile}-sts profile!")
click.echo(f"Keys successfully rotated for the {ctx.profile} profile")


if __name__ == "__main__":
main()
main(obj={})
54 changes: 54 additions & 0 deletions aws_iam_login/access_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from datetime import datetime
from typing import Dict, Union, Optional

from aws_iam_login.credentials import Credentials


class AccessKey:
"""
Understands the AccessKey format of AWS
"""

def __init__(self, data: Optional[dict]):
sanitized_data = self.__sanitize_input(data)
self.__data = sanitized_data if sanitized_data else {}
self.__credentials = Credentials(data)

@staticmethod
def __sanitize_input(data: Optional[dict]) -> Optional[dict]:
if not isinstance(data, dict):
return None

sanitized = {}

for key in ["UserName", "Status", "CreateDate"]:
if key in data:
sanitized[key] = data[key]

return sanitized

@property
def username(self) -> str:
return str(self.__data.get("UserName", ""))

@property
def credentials(self) -> Credentials:
return self.__credentials

@property
def status(self) -> str:
return str(self.__data.get("Status", ""))

@property
def created(self) -> datetime:
date = self.__data.get("CreateDate")
return date if isinstance(date, datetime) else datetime.now()

def __str__(self) -> str:
postfix = ""
if self.credentials.aws_secret_access_key:
postfix = " (contains secret)"
return f"{self.credentials.aws_access_key_id}, {self.status} {self.username} created at {self.created}{postfix}"

def __repr__(self) -> str:
return str(self)
Empty file.
36 changes: 36 additions & 0 deletions aws_iam_login/actions/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Optional
from abc import ABC, abstractmethod
from aws_iam_login.application_context import ApplicationMessages


class Action(ABC):
"""
Understands generic actions that are executed
"""

__subject: Optional[ApplicationMessages] = None

def subscribe_subject(self, subject: ApplicationMessages):
self.__subject = subject

def info(self, message: str) -> None:
if self.__subject:
self.__subject.send(message_type="INFO", message=message)

def warning(self, message: str) -> None:
if self.__subject:
self.__subject.send(message_type="WARN", message=message)

def error(self, message: str) -> None:
if self.__subject:
self.__subject.send(message_type="ERROR", message=message)

def exception(self, message: str, exception: Exception) -> None:
if self.__subject:
self.__subject.send(
message_type="ERROR", message=message, exception=exception
)

@abstractmethod
def execute(self) -> bool:
pass
41 changes: 41 additions & 0 deletions aws_iam_login/actions/initialize_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Optional
import boto3

from aws_iam_login.actions.action import Action
from aws_iam_login.aws_config import AWSConfig


class InitializeConfiguration(Action):
"""
Understands how to configure the AWS profiles to be used by aws-iam-login.
"""

__cached_client: Optional[boto3.Session.client] = None

def __init__(self, profile: str) -> None:
self.__profile = profile

self.__config = AWSConfig(profile)
self.__client = boto3.Session(profile_name=profile).client("sts")

def execute(self) -> bool:
if self.__config.mfa_serial and self.__config.username:
self.info(f"The {self.__profile} profile is already initialized.")
return True

try:
data = self.__client.get_caller_identity()
mfa_serial = data["Arn"].replace(":user/", ":mfa/")
username = mfa_serial.split("/")[-1]
self.info(f"Determined MFA ARN: {mfa_serial}")
self.info(f"Determined IAM Username: {username}")
self.__config.initialize(username=username, mfa_serial=mfa_serial)

return True
except Exception as exc:
message = (
f"Failed to get the caller identity for the {self.__profile} profile."
)
self.exception(message=message, exception=exc)

return False

0 comments on commit d1ffac5

Please sign in to comment.