package Amazon::Credentials;

use strict;
use warnings;
use 5.010;

use parent qw/Class::Accessor::Fast/;

__PACKAGE__->follow_best_practice;

__PACKAGE__->mk_accessors(
  qw/aws_secret_access_key aws_access_key_id token region
    user_agent profile debug expiration role container order
    serialized logger timeout insecure
    /
);

use Carp;
use Config::Tiny;
use Data::Dumper;
use Date::Format;
use English qw {-no_match_vars};
use Exporter;
use File::chdir;
use File::HomeDir;
use HTTP::Request;
use JSON::PP;
use LWP::UserAgent;
use POSIX::strptime qw/strptime/;
use Time::Local;
use Scalar::Util qw/reftype/;

use constant AWS_IAM_SECURITY_CREDENTIALS_URL => ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
  'http://169.254.169.254/latest/meta-data/iam/security-credentials/';
use constant AWS_AVAILABILITY_ZONE_URL => ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
  'http://169.254.169.254/latest/meta-data/placement/availability-zone';
use constant AWS_CONTAINER_CREDENTIALS_URL => 'http://169.254.170.2'; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant DEFAULT_SEARCH_ORDER => 'env,container,role,file'; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant DEFAULT_TIMEOUT => 3; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant DEFAULT_WINDOW_INTERVAL => 5; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant SECONDS_IN_MINUTE => 60; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant SECONDS_IN_HOUR => 3600; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)
use constant INSECURE_MODE => 2; ## no critic (ValuesAndExpressions::ProhibitConstantPragma)

our $VERSION = '1.0.19';

# we only log at debug level, create a default logger
{
  no strict 'refs';    ## no critic (TestingAndDebugging::ProhibitNoStrict)

  *{'Amazon::Credentials::Logger::debug'} = sub {
    my ( $self, @message ) = @_;

    return if !$self->{debug};

    my @tm = localtime time;
    print {*STDERR} sprintf " %s [%s] %s\n", strftime( '%c', @tm ),
      $PROCESS_ID,
      @message;
  };
}

sub new {
  my ( $class, @args ) = @_;
  my $options = ref( $args[0] ) ? $args[0] : {@args};

  my $self = $class->SUPER::new($options);

  $self->set_debug( $ENV{DEBUG} // $self->get_debug );

  if ( !$self->get_logger || reftype( $self->get_logger ) ne 'CODE' ) {
    $self->set_logger( bless { debug => $self->get_debug },
      'Amazon::Credentials::Logger' );
    $self->get_logger->debug('WARNING: DEBUG mode using default logger');
  }

  if ( !$self->get_user_agent ) {
    # set a very low timeout
    $self->set_user_agent(
      LWP::UserAgent->new( timeout => $self->get_timeout || DEFAULT_TIMEOUT )
    );
  } ## end if ( !$self->get_user_agent)

  if ( !$self->get_profile ) {
    $self->set_profile( $ENV{AWS_PROFILE} );
  }

  if ( !$self->get_order ) {
    $self->set_order(DEFAULT_SEARCH_ORDER);
  }

  $self->get_logger->debug( 'order ' . $self->get_order );

  if ( $self->get_insecure ) {
    $self->get_logger->debug( "!! CAUTION !!\n"
        . "!! You are executing in 'insecure' mode !!\n"
        . "!! Credentials may be exposed in debug messages !!\n" );
  }

  if ( !$self->get_aws_secret_access_key || !$self->get_aws_access_key_id ) {
    $self->set_credentials;
  }

  if ( !$self->get_region ) {
    $self->set_region( $ENV{AWS_REGION}
        || $ENV{AWS_DEFAULT_REGION}
        || $self->get_default_region );
  }

  return $self;
} ## end sub new

sub get_default_region {
  my $self = shift;

  # try to get credentials from instance role, but we may not be
  # executing on an EC2 or container.
  my $url = AWS_AVAILABILITY_ZONE_URL;

  my $ua = ref($self) ? $self->get_user_agent : LWP::UserAgent->new();

  my $req = HTTP::Request->new( GET => $url );

  my $region = eval {
    my $rsp = $ua->request($req);

    # if not 200, then get out of Dodge
    croak "could not get availability zone\n"
      if !$rsp->is_success;

    my $content = $rsp->content;
    $content =~ s/(\d+)[[:lower:]]+$/$1/xsm;

    return $content;
  };

  return $region;
} ## end sub get_default_region

sub set_credentials {
  my ( $self, $creds ) = @_;

  $creds = $creds || $self->find_credentials;

  if ( $creds->{aws_secret_access_key} && $creds->{aws_access_key_id} ) {

    $self->set_aws_secret_access_key( $creds->{aws_secret_access_key} );
    $self->set_aws_access_key_id( $creds->{aws_access_key_id} );
    $self->set_token( $creds->{token} );
    $self->set_expiration( $creds->{expiration} );
  } ## end if ( $creds->{aws_secret_access_key...})
  else {
    croak "no credentials available\n";
  }

  return $creds;
} ## end sub set_credentials

sub get_ec2_credentials {
  goto &find_credentials;
}

sub find_credentials {
  my ( $self, @args ) = @_;

  my $options = ref( $args[0] ) ? $args[0] : {@args};

  if ( $options->{profile} ) {
    $self->set_profile( $options->{profile} );
  }

  if ( $options->{order} ) {
    $self->set_order( $options->{order} );
  }

  my @search_order;

  if ( $self->get_profile ) {
    @search_order = ('file');
  }
  elsif ( ref( $self->get_order ) && reftype( $self->get_order ) eq 'ARRAY' )
  {
    @search_order = @{ $self->get_order };
  }
  elsif ( !ref( $self->get_order ) ) {
    @search_order = split /\s*,\s*/xsm, $self->get_order;
  }

  my $creds = {};

  my %creds_getters = (
    env => sub {
      if ( $ENV{AWS_ACCESS_KEY_ID} && $ENV{AWS_SECRET_ACCESS_KEY} ) {
        @{$creds}{qw/source aws_access_key_id aws_secret_access_key token/}
          = (
          'ENV',
          @ENV{qw/AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN/}
          );
      } ## end if ( $ENV{AWS_ACCESS_KEY_ID...})

      return $creds;
    },
    role => sub {
      return $self->get_creds_from_role;
    },
    container => sub {
      return $self->get_creds_from_container;
    },
    file => sub {
      return $self->get_creds_from_ini_file;
    },
  );

  foreach my $location (@search_order) {
    $self->get_logger->debug( 'searching for credentials: ' . $location );

    if ( $creds_getters{$location} ) {
      $creds = $creds_getters{$location}->();
    }

    last if $creds->{source};
  } ## end foreach my $location (@search_order)

  foreach my $k ( keys %{$creds} ) {
    $self->set( $k, $creds->{$k} );
  }

  return $creds;
} ## end sub find_credentials

sub get_creds_from_file { ## no critic (Subroutines::ProhibitExcessComplexity)
  my ( $self, $profile ) = @_;

  $profile = $profile || $self->get_profile || 'default';

  my $creds = {};

  foreach my $config (qw{ .aws/config .aws/credentials }) {
    my $profile_name = $profile;

    local $CWD = home;    ## no critic (Variables::ProhibitLocalVars)

    next if !-e $config;

    my $last_profile    = q{};
    my $profile_to_find = $profile_name;

    # look for credentials...by interating through credentials file
    # (could use Config::INI but did not want to add another
    # dependency), maybe Config::Tiny?
    open my $fh, '<', $config    ## no critic (InputOutput::RequireBriefOpen)
      or croak 'could not open credentials file!';

    while ( my $current_line = <$fh> ) {

      chomp $current_line;

      $self->get_logger->debug(
        'looking for profile ' . ( $profile_name // q{} ) );

      # once we find a profile section that matches, undef it
      # ./aws/credentials uses [profile-name]
      # ./aws/config uses [profile profile-name]

      if ( $current_line =~ /^\s*region\s*=\s*(.*?)\s*$/xsm ) {
        my $region = $1;
        # go ahead and use this region setting IF:
        # 1. this is the default profile (we may reset region later though)
        # 2. the profile we want to use is this profile
        # 3. we are not in a profile at all (this is yet another default)
        if ( $last_profile =~ /default/sm
          || ( $profile_to_find && $last_profile =~ /$profile_to_find/xsm )
          || !$last_profile ) {
          $self->set_region($region);
        }
      } ## end if ( $current_line =~ ...)

      if ($profile_name) { ## no critic (ControlStructures::ProhibitCascadingIfElse)
        if ( $current_line =~ /^\s*\[\s*profile\s+$profile_name\s*\]/xsm ) {
          undef $profile_name;
        }
        elsif ( $current_line =~ /^\s*\[\s*$profile_name\s*\]/xsm ) {
          undef $profile_name;
          $last_profile = $current_line;
        }
      } ## end if ($profile_name)
      elsif ( defined $profile_name
        && $current_line =~ /^\s*\[\s*profile\s+/xsm ) {
        last;
      }
      elsif ( $current_line =~ /credential_process/xsm ) {
        my $process;
        if ( $current_line =~ /\=(.*)$/xsm ) {
          $process = $1;
        }

        @{$creds}{qw{source aws_access_key_id aws_secret_access_key token}}
          = ( 'process', $self->get_creds_from_process($process) );

        last;
      } ## end elsif ( $current_line =~ ...)
      elsif ( $current_line =~ /^\s*aws_secret_access_key\s*=\s*(.*)$/xsm ) {
        last if defined $creds->{aws_secret_access_key};    # next profile
        $creds->{aws_secret_access_key} = $1;
      }
      elsif ( $current_line =~ /^\s*aws_access_key_id\s*=\s*(.*)$/xsm ) {
        last if defined $creds->{aws_access_key_id};        # next profile
        $creds->{aws_access_key_id} = $1;
      }
      elsif ( $current_line =~ /^\s*aws_session_token\s*=\s*(.*)$/xsm ) {
        last if defined $creds->{token};
        $creds->{token} = $1;
      }
      elsif ( $current_line =~ /^\s*profile\s*=\s*(.*)$/xsm ) {
        $profile_name = $1;
        seek $fh, 0, 1;
      }
    } ## end while ( my $current_line ...)

    close $fh
      or croak "could close filehandle for $config\n";

    if ( $creds->{aws_secret_access_key} && $creds->{aws_access_key_id} ) {
      $creds->{source} = $config;
    }
  } ## end foreach my $config (qw{ .aws/config .aws/credentials })

  return $creds;
} ## end sub get_creds_from_file

sub dump_ini_file {
  my ( $self, $ini ) = @_;

  my $safe_rsp;

  if ( $self->get_insecure && $self->get_insecure =~ /^2$/xsm ) {
    $safe_rsp = Dumper [$ini];
  }
  elsif ($self->get_insecure) {
    $safe_rsp = Dumper [$ini];
    $safe_rsp =~ s/(aws_[^']+)'([^']+)'([^']+)'/$1'$2'...'/gxsm;
  }
  else{
    $safe_rsp = '** configuration contents blocked by insecure setting **';
  }
  

  return $safe_rsp;
} ## end sub dump_ini_file

sub get_creds_from_ini_file {
  my ( $self, $profile ) = @_;

  $profile = $profile || $self->get_profile || 'default';

  my $creds = {};
  my $cwd   = $CWD;

  foreach my $config (qw{ .aws/config .aws/credentials }) {
    last if $creds->{source};

    my $profile_name = $profile;

    $CWD = home;

    next if !-e $config;

    my $ini = Config::Tiny->read($config);

    $self->get_logger->debug( $self->dump_ini_file($ini) );

    if ( $profile eq 'default' ) {
      next if !$ini->{default}->{profile};

      $profile_name = $ini->{default}->{profile};
    }

    if ( $ini->{region} ) {
      $self->set_region( $ini->{region} );
    }

    my $section;

    if ( $ini->{$profile_name} ) {
      $section = $ini->{$profile_name};
    }
    elsif ( $ini->{"profile $profile_name"} ) {
      $section = $ini->{"profile $profile_name"};
    }

    if ( $ini->{credential_process} ) {
      @{$creds}
        {qw{source aws_access_key_id aws_secret_access_key token region}} = (
        'process', $self->get_creds_from_process( $ini->{credential_process} )
        );
    } ## end if ( $ini->{credential_process...})

    if ($section) {
      if ( $section->{credential_process} ) {
        @{$creds}
          {qw{source aws_access_key_id aws_secret_access_key token region}}
          = (
          'process',
          $self->get_creds_from_process( $section->{credential_process} )
          );
      } ## end if ( $section->{credential_process...})
      else {
        foreach
          my $k (qw{aws_access_key_id aws_secret_access_key token region}) {
          $creds->{$k} = $section->{$k};
        }
      } ## end else [ if ( $section->{credential_process...})]

      if ( $creds->{aws_access_key_id} ) {
        $creds->{source} = $config;
      }
    } ## end if ($section)
  } ## end foreach my $config (qw{ .aws/config .aws/credentials })

  $CWD = $cwd;

  if ( $creds->{region} ) {
    $self->set_region( $creds->{region} );
  }

  return $creds;
} ## end sub get_creds_from_ini_file

sub is_token_expired {
  my ( $self, $window_interval ) = @_;
  $window_interval = $window_interval // DEFAULT_WINDOW_INTERVAL;

  my $expiration_date = $self->get_expiration();

  my $expired = 0;

  if ( defined $expiration_date ) {
    # AWS recommends getting credentials 5 minutes prior to expiration
    my $g = _iso8601_to_time($expiration_date);

    # shave 5 minutes or window interval off of the expiration time
    $g -= $window_interval * SECONDS_IN_MINUTE;

    # (expiration_time - window_interval) - current_time = # of seconds left before expiration
    my $seconds_left = $g - time;

    if ( $self->get_debug ) {
      $self->get_logger->debug("seconds left : $seconds_left");
      my $hours   = int( $seconds_left / SECONDS_IN_HOUR );
      my $minutes = int(
        ( $seconds_left - $hours * SECONDS_IN_HOUR ) / SECONDS_IN_MINUTE );
      my $seconds = $seconds_left
        - ( $hours * SECONDS_IN_HOUR + $minutes * SECONDS_IN_MINUTE );
      $self->get_logger->debug(
        "$hours hours $minutes minutes $seconds seconds until expiry");
    } ## end if ( $self->get_debug )

    $expired = ( $seconds_left < 0 ) ? 1 : 0;

    $self->get_logger->debug(
      Dumper [ 'EXPIRATION TIME: ' . $expiration_date,
        'EXPIRED: ' . $expired ] );
  } ## end if ( defined $expiration_date)

  return $expired;
} ## end sub is_token_expired

sub _iso8601_to_time {
  my $iso8601 = shift;

  $iso8601 =~ s/^(.*)Z$/$1\+00:00/xsm;

  my $gmtime = eval {
    local $ENV{TZ} = 'GMT';

    timegm( strptime( $iso8601, '%Y-%m-%dT%H:%M:%S%z' ) );
  };

  return $gmtime;
} ## end sub _iso8601_to_time

sub dump_response {
  my ( $self, $rsp ) = @_;

  my $safe_rsp;

  if ( $self->get_insecure && $self->get_insecure =~ /^2$/xsm ) {
    $safe_rsp = $rsp;
  }
  elsif ( $self->get_insecure ) {
    $safe_rsp = {};

    foreach my $k ( keys %{$rsp} ) {
      if ( $k =~ /content/xsm ) {
        my $content = $rsp->{$k};
        $content
          =~ s/\"(AccessKeyId|Token|SecretAccessKey)\"\s+\:\s+\"[^\"]+\"/\"$1\" : \"...\"/gxsm;
        $safe_rsp->{$k} = $content;
      } ## end if ( $k =~ /content/xsm)
      else {
        $safe_rsp->{$k} = $rsp->{$k};
      }
    } ## end foreach my $k ( keys %{$rsp...})
  } ## end elsif ( $self->get_insecure)
  else {
    $safe_rsp = '** HTTP RESPONSE blocked by insecure setting **';
  }

  return Dumper [$safe_rsp];
} ## end sub dump_response

sub get_creds_from_role {
  my ($self) = @_;

  # try to get credentials from instance role
  my $url = AWS_IAM_SECURITY_CREDENTIALS_URL;

  my $ua = $self->get_user_agent;
  my $role;

  my $creds = {};

  $creds = eval {
    # could be infinite, but I don't think so.  Either we get an
    # error ($@), or a non-200 response code
    while ( !$creds->{token} ) {

      if ($role) {
        $url .= $role;
      }

      my $req = HTTP::Request->new( GET => $url );

      $self->get_logger->debug( Dumper [ "HTTP REQUEST:\n", $req ] );

      my $rsp = $ua->request($req);

      $self->get_logger->debug( $self->dump_response($rsp) );

      # if not 200, then get out of Dodge
      last if !$rsp->is_success;

      if ($role) {
        $creds->{serialized} = $rsp->content;
        my $this = JSON::PP->new->utf8->decode( $creds->{serialized} );
        @{$creds}{
          qw/source role aws_access_key_id aws_secret_access_key token expiration/
          } = (
          'IAM', $role,
          @{$this}{qw/AccessKeyId SecretAccessKey Token Expiration/},
          );
      } ## end if ($role)
      else {
        $role = $rsp->content;
        $self->get_logger->debug( Dumper [ 'role', $role ] );

        last if !$role;
      } ## end else [ if ($role) ]
    } ## end while ( !$creds->{token} )

    return $creds;
  };

  if ($EVAL_ERROR) {
    $creds->{error} = $EVAL_ERROR;
  }
  else {
    $creds->{error} = undef;
  }

  return $creds;
} ## end sub get_creds_from_role

sub get_creds_from_process {
  my ( $self, $process ) = @_;

  open my $fh, q{-|}, $process
    or die "could not open pipe to $process\n";

  local $RS = undef;

  my $credentials = <$fh>;
  close $fh
    or croak "could not close filehandle on $process\n";

  $credentials = eval { JSON::PP->new->decode($credentials); };

  if ( $EVAL_ERROR || !$credentials ) {
    die "could not get credentials from process\n$EVAL_ERROR\n";
  }

  return @{$credentials}
    {qw/AccessKeyId SecretAccessKey SessionToken Region Expiration/};
} ## end sub get_creds_from_process

sub refresh_credentials {
  goto &refresh_token;
}

sub refresh_token {
  my ($self) = @_;
  my $creds;

  if ( $self->get_container && $self->get_container eq 'ECS' ) {
    $creds = $self->get_creds_from_container;
  }
  elsif ( $self->get_role ) {
    $creds = $self->get_creds_from_role;
  }

  croak 'unable to refresh token!'
    if !ref($creds) || !keys %{$creds};

  return $self->set_credentials($creds);
} ## end sub refresh_token

sub credential_keys {
  my ($self) = @_;

  my %credential_keys;

  if ( $self->get_aws_access_key_id
    && $self->get_aws_secret_access_key ) {

    %credential_keys = (
      AWS_ACCESS_KEY_ID     => $self->get_aws_access_key_id,
      AWS_SECRET_ACCESS_KEY => $self->get_aws_secret_access_key,
    );

    if ( $self->get_token ) {
      $credential_keys{AWS_SESSION_TOKEN} = $self->get_token;
    }
  } ## end if ( $self->get_aws_access_key_id...)

  return \%credential_keys;
} ## end sub credential_keys

sub as_string {
  my ($self) = @_;

  return JSON::PP->new->pretty->encode( $self->credential_keys );
}

sub format_credentials {
  my ( $self, $format ) = @_;

  $format = $format || "%s %s\n";

  my $credential_keys = $self->credential_keys;

  return join q{}, map { sprintf $format, $_, $credential_keys->{$_} }
    keys %{$credential_keys};
} ## end sub format_credentials

sub get_creds_from_container {
  my ( $self, $uri ) = @_;

  $uri = $uri || $ENV{AWS_CONTAINER_CREDENTIALS_RELATIVE_URI};

  my $creds = {};

  if ($uri) {
    $self->get_logger->debug( caller(2), $uri );

    $creds = eval {
      # try to get credentials from instance role
      my $url = AWS_CONTAINER_CREDENTIALS_URL . $uri;

      my $ua = $self->get_user_agent;
      my $req = HTTP::Request->new( GET => $url );
      $req->header(qw{ Accept */* });

      $self->get_logger->debug( Dumper [ "HTTP REQUEST:\n", $req ] );

      $self->get_logger->debug( Dumper [ $req->as_string ] );

      my $rsp = $ua->request($req);

      $self->get_logger->debug( $self->dump_response($rsp) );

      # if not 200, then get out of Dodge
      if ( $rsp->is_success ) {

        $creds->{serialized} = $rsp->content;

        my $this = JSON::PP->new->utf8->decode( $rsp->content );

        @{$creds}{
          qw/source container aws_access_key_id aws_secret_access_key token expiration/
          } = (
          'IAM', 'ECS',
          @{$this}{qw/AccessKeyId SecretAccessKey Token Expiration/},
          );
      } ## end if ( $rsp->is_success )
      else {
        $self->get_logger->debug( 'return code: ' . $rsp->status_line );
      }

      return $creds;
    };

    if ($EVAL_ERROR) {
      $creds->{error} = $EVAL_ERROR;
    }
    else {
      $creds->{error} = $EVAL_ERROR;
    }
  } ## end if ($uri)

  return $creds;
} ## end sub get_creds_from_container

1;

__END__

=pod

=head1 NAME

 Amazon::Credentials

=head1 SYNOPSIS

 my $aws_creds = Amazon::Credentials->new({order => [qw/env file container role/]});

=head1 DESCRIPTION

Class to find AWS credentials from either the environment,
configuration files, instance meta-data or container role.

You can specify the order using the C<order> option in the constructor
to determine the order in which the class will look for credentials.
The default order is I<environent>, I<file>, I<container>, I<instance
meta-data>. See L</new>.

=head1 SUBROUTINES/METHODS

=head2 new

 new( options );

 my $aws_creds = Amazon::Credential->new( { profile => 'sandbox', debug => 1 });

C<options> is a hash of keys that represent various options you can
pass to the constructor to control how it will look for credentials.
Any of the options can also be retrieved using their corresponding
'get_{option} method.

=head3 options

=over 5

=item aws_access_key_id

AWS access key.

=item aws_secret_access_key

AWS secret access key.

I<Note: If you pass the access keys in the constructor then the
constructor will not look in other places for credentials.>

=item container - Task Role

If the process is running in a container, the container may have a
task role.  The class will look for credentials using the container
metadata service:

 http://169.254.170.2/$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

=item debug

Set to true for verbose troubleshooting information.  Set C<logger> to
a logger that implements a logging interface (ala
C<Log::log4perl>. Debug output canalso be enabled by setting the
environment variable C<DEBUG> to a I<true>.

=item env - Environment

If there exists an environment variable $AWS_PROFILE, then an attempt
will be made to retrieve credentials from the credentials file using
that profile, otherwise the class will for these environment variables
to provide credentials.

 AWS_ACCESS_KEY_ID
 AWS_SECRET_ACCESS_KEY
 AWS_SESSION_TOKEN

I<Note that when you set the environment variable AWS_PROFILE, the
order essentially is overridden and the class will ook in your
credential files (F<~/.aws/config>, F<~/.aws/credentials>) to resolve
your credentials.>

=item file - Configuration Files

=over 15

=item ~/.aws/config

=item ~/.aws/credentials

=back

The class will attempt to resolve credentials by interpretting the
information in these two files. You can also specify a profile to use
for looking up the credentials by passing it into the constructor or
setting it the environment variable C<AWS_PROFILE>.  If no profile is
provided, the default credentials or the first profile found is used.

 my $aws_creds = Amazon::Credentials->new({ order => [qw/environment role file/] });

=item insecure

DEBUG mode can be enabled to display information that may aid in
troubleshooting, however output may include credentials.  This
attribute prevents accidental exfiltration of credentials during
troubleshooting.  The default setting of C<insecure> is therefore
false. This will prevent debug messages that may contain credentials
(HTTP response, configuration file contents) from exposing sensitive
data.

Set the value to 1 to enable all debug output B<except> the content of
credentials in HTTP responses. Set the value to 2 to enable full debug
output.

I<Note that setting the value to 1 will enable the use of regular
expressions to suppress credential contents. Credentials that do not
conform to these may still be exposed. Caution is advised.>


=item logger

Pass in your own logger that has a C<debug()> method.  Otherwise the
default logger will output debug messages to STDERR.

=item order

An array reference containing tokens that specifies the order in which the class will
search for credentials.

default:  env, role, container, file

Example:

  my $creds = Amazon::Credentials->new( { order => [ qw/file env role/] });

=over 10

=item profile

The profile name in the configuration file (F<~/.aws/config> or
F<~/.aws/credentials>).

 my $aws_creds = Amazon::Credentials->new({ profile => 'sandbox' });

The class will also look for the environment variable C<AWS_PROFILE>,
so you can invoke your script like this:

 $ AWS_PROFILE=sandbox my-script.pl

=item region

Default region. The class will attempt to find the region in either
the configuration files or the instance unless you specify the region
in the constructor.

=item role - Instance Role

The class will use the
I<http://169.254.169.254/latest/meta-data/iam/security-credential> URL
to look for an instance role and credentials.

Credentials returned by accessing the meta-data include a token that
should be passed to Amazon APIs along with the access key and secret.
That token has an expiration and should be refreshed before it
expires.

 if ( $aws_creds->is_token_expired() ) {
   $aws_creds->refresh_token()
 }

=back

=item timeout

When looking for credentials in metadata URLs, this parameter
specifies the timeout value for C<LWP>.  The default is 3 seconds.

=item user_agent

Pass in your own user agent, otherwise LWP will be used. I<Probably>
only useful to override this for testing purposes.>

=back

=head2 as_string

 as_string()

Returns the credentials as a JSON encode string.

=head2 credential_keys

 my $credential_keys = $creds->credential_keys;

Return a hash reference containing the credential keys with standard
key names.  Note that the session token will only be present in the
hash for temporary credentials.

=over 5

=item AWS_ACCESS_KEY_ID

=item AWS_SECRET_ACCESS_KEY

=item AWS_SESSION_TOKEN

=back

=head2 format_credentials

 format_credentials(format-string)

Returns the credentials as a formatted string.  The <format> argument
allows you to include a format string that will be used to output each
of the credential parts.

 format("export %s=%s\n");

The default format is a "%s %s\n".

=head2 find_credentidals

 find_credentials( option => value, ...);

You normally don't want to use this method. It's automatically invoked
by the constructor if you don't pass in any credentials. Accepts a
hash or hash reference consisting of keys (C<order> or C<profile>) in
the same manner as the constructor.

=head2 get_creds_from_*

These methods are called internally when the C<new> constructor is
invoked. You should not normally call these methods

=head3 get_creds_from_container

 get_creds_from_container()

Retrieves credentials from the container's metadata at
http://169.254.170.2.  Returns a hash of credentials containing:

  aws_access_key_id
  aws_secret_access_key
  aws_session_token

Returns an empty hash if no credentials found.  The environment
variable C<AWS_CONTAINER_CREDENTIALS_RELATIVE_URI> must exist or you
must pass the value of the path as an argument.

=head3 get_creds_from_process

 get_creds_from_process(process)

Retrieves credentials from a helper process defined in the config
file. Returns the credentials tuple.

=head3 get_creds_from_role

 get_creds_from_role()

Returns a hash, possibly containing access keys and a token.

=over 5

=item aws_access_key_id

The AWS access key.

=item aws_secret_access_key

The AWS secret key.

=item token

Security token used with access keys.

=item expiration

Token expiration date.

=item role

IAM role if available.

=item source

Will be 'IAM' if role and credentials found.

=back

=head2 get_default_region

Returns the region of the currently running instance.  The constructor
will set the region to this value unless you set your own C<region>
value.  Use C<get_region> to retrieve the value after instantiation or
you can call this method again and it will make a second call to
retrieve the instance metadata.

You can also invoke this as a class method:

 AWS_REGION=$(perl -MAmazon::Credentials -e 'print Amazon::Credentials::get_default_region;')

=head2 get_ec2_credentials (deprecated)

See L</find_credentials>

=head2 is_token_expired

 is_token_expired( window-interval )

Returns true if the token is about to expire (or is
expired). C<window-interval> is the time in minutes before the actual
expiration time that the method should consider the token expired.
The default is 5 minutes.  Amazon states that new credentials will be
available I<at least> 5 minutes before a token expires.

=head2 refresh_token

 refresh_token() (deprecated)
 refresh_credentials()

Retrieves a fresh set of IAM credentials.

 if ( $creds->is_token_expired ) {
   $creds->refresh_token()
 }

=head1 SETTERS/GETTERS

All of the parameters described in the new method can be accessed by a
I<getter> or set using a I<setter>.

=head1 CONTRIBUTING

You can find this project on GitHub at
L<https://github.com/rlauer6/perl-Amazon-Credentials>.  PRs are always
welcomed!

=head1 VERSION

This document reverse to verion @PACKAGE_VERION@ of
C<Amazon::Credentials>.

=head1 DIAGNOSTICS

Set the environment variable DEBUG or pass a true value for the debug
attribute in the constructor to output debug and diagnostic messages.

=head1 CONFIGURATION AND ENVIRONMENT

The module will recognize severa AWS specific environment variables
describe throughout this documentation.

=over 5

=item AWS_ACCESS_KEY_ID

=item AWS_SECRET_ACCESS_KEY

=item AWS_SESSION_TOKEN

=item AWS_REGION

=item AWS_DEFAULT_REGION

=item AWS_CONTAINER_CREDENTIALS_RELATIVE_URI

=back

You can also set the environment variable C<DEBUG> to any true value
to output debugging information.

=head1 BUGS AND LIMITATIONS

C<Amazon::Credentials> will B<not> attempt to retrieve temporary
credentials for profiles that specify a role. If you, for example you
define a role in your credentials file:

 [developer]

  role_arn = arn:aws:iam::123456789012:role/developer-access-role
  source_profile = dev

The module will not return credentials for the I<developer>
profile.

=head1 DEPENDENCIES

Dependencies as report by C<scandeps.pl>...however lower versions of
these modules are probably acceptable.

 'Carp'                          => '1.26',
 'Class::Accessor::Fast'         => '0.31',
 'Config::Tiny'                  => '2.28',
 'Data::Dumper'                  => '2.145',
 'Date::Format'                  => '2.24',
 'Exporter'                      => '5.68',
 'File::HomeDir'                 => '1.00',
 'File::chdir'                   => '0.1010',
 'HTTP::Request'                 => '6.00',
 'JSON::PP'                      => '2.97001',
 'LWP::UserAgent'                => '6.36',
 'POSIX::strptime'               => '0.13',
 'Scalar::Util'                  => '1.50',
 'Time::Local'                   => '1.2300',
 'constant'                      => '1.33',
 'parent'                        => '0.225',

=head1 SECURITY CONSIDERATIONS

Once credentials are resolved, they will be stored in plain-text
within this object. The security concern around this fact is not
actually the fact hat the credentials are in plain-text. Any process
that compromises your environment can use the same methods this class
does to resolve those credentials. The concern is that once you have
resolved these credentials you may inadvertantly explose them in other
ways. Dumping the object to logs, files or the display will expose
your credentials.

B<Always take precautions to prevent accidental exfiltration of your
credentials.>

=over 5

=item * Consider removing them from the hash once they have been
resolved and used by your application. You can regenerate them using
the C<find_credentials> method when needed.

=item * Consider using encryption to encrypt the values in memory to
prevent accidental exposure.

=back

=head1 INCOMPATIBILITIES

This module has not been tested on Windows OS.

=head1 LICENSE AND COPYRIGHT

This module is free software. It may be used, redistributed and/or
modified under the same terms as Perl itself.

=head1 AUTHOR

Rob Lauer - <rlauer6@comcast.net>

=cut
