Skip to content

Commit

Permalink
Merge pull request #9605 from hashicorp/keybase-aws-login-profile
Browse files Browse the repository at this point in the history
provider/aws: aws_iam_user_login_profile resource
  • Loading branch information
jen20 authored Oct 26, 2016
2 parents 836d171 + e5bda11 commit eb17741
Show file tree
Hide file tree
Showing 56 changed files with 14,873 additions and 0 deletions.
1 change: 1 addition & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ func Provider() terraform.ResourceProvider {
"aws_iam_user_policy": resourceAwsIamUserPolicy(),
"aws_iam_user_ssh_key": resourceAwsIamUserSshKey(),
"aws_iam_user": resourceAwsIamUser(),
"aws_iam_user_login_profile": resourceAwsIamUserLoginProfile(),
"aws_instance": resourceAwsInstance(),
"aws_internet_gateway": resourceAwsInternetGateway(),
"aws_key_pair": resourceAwsKeyPair(),
Expand Down
182 changes: 182 additions & 0 deletions builtin/providers/aws/resource_aws_iam_user_login_profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package aws

import (
"encoding/base64"
"errors"
"fmt"
"log"
"math/rand"
"strings"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/vault/helper/pgpkeys"
)

func resourceAwsIamUserLoginProfile() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIamUserLoginProfileCreate,
Read: schema.Noop,
Update: schema.Noop,
Delete: schema.RemoveFromState,

Schema: map[string]*schema.Schema{
"user": {
Type: schema.TypeString,
Required: true,
},
"pgp_key": {
Type: schema.TypeString,
Required: true,
},
"password_reset_required": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"password_length": {
Type: schema.TypeInt,
Optional: true,
Default: 20,
ValidateFunc: validateAwsIamLoginProfilePasswordLength,
},

"key_fingerprint": {
Type: schema.TypeString,
Computed: true,
},
"encrypted_password": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func validateAwsIamLoginProfilePasswordLength(v interface{}, _ string) (_ []string, es []error) {
length := v.(int)
if length < 4 {
es = append(es, errors.New("minimum password_length is 4 characters"))
}
if length > 128 {
es = append(es, errors.New("maximum password_length is 128 characters"))
}
return
}

// generatePassword generates a random password of a given length using
// characters that are likely to satisfy any possible AWS password policy
// (given sufficient length).
func generatePassword(length int) string {
charsets := []string{
"abcdefghijklmnopqrstuvwxyz",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"012346789",
"!@#$%^&*()_+-=[]{}|'",
}

// Use all character sets
random := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
components := make(map[int]byte, length)
for i := 0; i < length; i++ {
charset := charsets[i%len(charsets)]
components[i] = charset[random.Intn(len(charset))]
}

// Randomise the ordering so we don't end up with a predictable
// lower case, upper case, numeric, symbol pattern
result := make([]byte, length)
i := 0
for _, b := range components {
result[i] = b
i = i + 1
}

return string(result)
}

func encryptPassword(password string, pgpKey string) (string, string, error) {
const keybasePrefix = "keybase:"

encryptionKey := pgpKey
if strings.HasPrefix(pgpKey, keybasePrefix) {
publicKeys, err := pgpkeys.FetchKeybasePubkeys([]string{pgpKey})
if err != nil {
return "", "", errwrap.Wrapf(
fmt.Sprintf("Error retrieving Public Key for %s: {{err}}", pgpKey), err)
}
encryptionKey = publicKeys[pgpKey]
}

fingerprints, encrypted, err := pgpkeys.EncryptShares([][]byte{[]byte(password)}, []string{encryptionKey})
if err != nil {
return "", "", errwrap.Wrapf(
fmt.Sprintf("Error encrypting password for %s: {{err}}", pgpKey), err)
}

return fingerprints[0], base64.StdEncoding.EncodeToString(encrypted[0]), nil
}

func resourceAwsIamUserLoginProfileCreate(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn

username := d.Get("user").(string)
passwordResetRequired := d.Get("password_reset_required").(bool)
passwordLength := d.Get("password_length").(int)

_, err := iamconn.GetLoginProfile(&iam.GetLoginProfileInput{
UserName: aws.String(username),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() != "NoSuchEntity" {
// If there is already a login profile, bring it under management (to prevent
// resource creation diffs) - we will never modify it, but obviously cannot
// set the password.
d.SetId(username)
d.Set("key_fingerprint", "")
d.Set("encrypted_password", "")
return nil
}
}

var pgpKey string
if pgpKeyInterface, ok := d.GetOk("pgp_key"); ok {
pgpKey = pgpKeyInterface.(string)
}

initialPassword := generatePassword(passwordLength)
fingerprint, encrypted, err := encryptPassword(initialPassword, pgpKey)
if err != nil {
return err
}

request := &iam.CreateLoginProfileInput{
UserName: aws.String(username),
Password: aws.String(initialPassword),
PasswordResetRequired: aws.Bool(passwordResetRequired),
}

log.Println("[DEBUG] Create IAM User Login Profile request:", request)
createResp, err := iamconn.CreateLoginProfile(request)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "EntityAlreadyExists" {
// If there is already a login profile, bring it under management (to prevent
// resource creation diffs) - we will never modify it, but obviously cannot
// set the password.
d.SetId(username)
d.Set("key_fingerprint", "")
d.Set("encrypted_password", "")
return nil
}
return errwrap.Wrapf(fmt.Sprintf("Error creating IAM User Login Profile for %q: {{err}}", username), err)
}

d.SetId(*createResp.LoginProfile.UserName)
d.Set("key_fingerprint", fingerprint)
d.Set("encrypted_password", encrypted)
return nil
}
Loading

0 comments on commit eb17741

Please sign in to comment.