Skip to content
This repository has been archived by the owner on Nov 8, 2021. It is now read-only.

Commit

Permalink
Include IAM policies with assume role transform (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdk committed Dec 9, 2020
1 parent 3df715a commit 8004ff7
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 10 deletions.
103 changes: 93 additions & 10 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
package config

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"

"gopkg.in/ini.v1"
)
Expand All @@ -34,6 +37,8 @@ type Role struct {
ExternalID string
MFAMessage string
MFASerial string
Policy string
PolicyARNs []string
RoleARN string
RoleSessionName string
SourceProfile string
Expand Down Expand Up @@ -72,7 +77,7 @@ func sectionAsUser(section *ini.Section) *User {

// sectionAsRole takes the given ini.Section and converts it to a Role if all
// of the required fields are present.
func sectionAsRole(section *ini.Section) *Role {
func sectionAsRole(section *ini.Section) (*Role, error) {
// Pack section values into struct.
// https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#using-aws-iam-roles
role := Role{
Expand All @@ -85,22 +90,30 @@ func sectionAsRole(section *ini.Section) *Role {
YubikeySlot: section.Key("yubikey_slot").Value(),
}

// Verify that required fields are present.
switch {
case role.RoleARN == "":
return nil, nil
case role.SourceProfile == "":
return nil, nil
}

// Use the given duration, or fall back to a 1 hour default.
if duration, err := section.Key("duration_seconds").Int(); err == nil {
role.DurationSeconds = duration
} else {
role.DurationSeconds = 3600 // 1 hour
}

// Verify that required fields are present.
switch {
case role.RoleARN == "":
return nil
case role.SourceProfile == "":
return nil
default:
return &role
// Read, parse, and combine the referenced policies.
policyARNs, policy, err := loadPolicies(section.Key("policies").Strings(",")...)
if err != nil {
return nil, err
}

role.PolicyARNs = policyARNs
role.Policy = policy
return &role, nil
}

// sectionAsSession takes the given ini.Section and converts it to a Session if
Expand Down Expand Up @@ -150,7 +163,10 @@ func (c *Config) Profile(name string) (*User, *Role, *Session, error) {
}

// Section contains valid Role settings.
if role := sectionAsRole(section); role != nil {
if role, err := sectionAsRole(section); err != nil {
// Section contains a Role, but role configuration was invalid.
return nil, nil, nil, err
} else if role != nil {
return nil, role, nil, nil
}

Expand Down Expand Up @@ -245,3 +261,70 @@ func userHomeDir() string {
// *nix
return os.Getenv("HOME")
}

// loadPolicies takes a given list of policy references (either an ARN, or a
// file path) and will return a list of all given ARNs, as well as a single IAM
// policy document that is a combination of all given policy files.
func loadPolicies(policyRefs ...string) ([]string, string, error) {
var documents [][]byte
var policyARNs []string
for _, policyRef := range policyRefs {
if policyRef == "" {
continue
}

// If a policy ARN was given, add it to the list of ARNs. Otherwise,
// assume it's a file and read the contents.
if strings.HasPrefix(policyRef, "arn:aws:iam:") {
policyARNs = append(policyARNs, policyRef)
} else {
document, err := ioutil.ReadFile(policyRef)
if err != nil {
return nil, "", err
}
documents = append(documents, document)
}
}

// Combine all policy documents into one.
combined, err := combinePolicies(documents...)
if err != nil {
return nil, "", err
}

return policyARNs, string(combined), nil
}

// combinePolicies takes a given list of JSON-encoded IAM policy documents, and
// returns a new policy document with all of the individual statements combined
// in order.
func combinePolicies(policies ...[]byte) ([]byte, error) {
// document is used for opaquely unmarshaling all policy statements.
type document struct {
Version string `json:"Version"`
Statements []interface{} `json:"Statement"`
}

var combined document
for _, policy := range policies {
// Unmarshal each policy document.
var doc document
if err := json.Unmarshal(policy, &doc); err != nil {
return nil, err
}

// Add each contained statement to the combined policy.
combined.Version = doc.Version
for _, statement := range doc.Statements {
combined.Statements = append(combined.Statements, statement)
}
}

// Avoid returning a policy with no statements.
if len(combined.Statements) == 0 {
return nil, nil
}

// Return the JSON-encoded combined policy (non-indented to save bytes).
return json.Marshal(combined)
}
10 changes: 10 additions & 0 deletions transformers/assume-role.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ func (s AssumeRoleTransform) Transform(creds *sts.Credentials) (*sts.Credentials
input.SerialNumber = aws.String(value)
}

if value := s.Role.Policy; value != "" {
input.Policy = aws.String(value)
}

for _, value := range s.Role.PolicyARNs {
input.PolicyArns = append(input.PolicyArns, &sts.PolicyDescriptorType{
Arn: aws.String(value),
})
}

if value := s.Role.RoleSessionName; value != "" {
input.RoleSessionName = aws.String(value)
} else {
Expand Down

0 comments on commit 8004ff7

Please sign in to comment.