Skip to content

Commit

Permalink
Implement a subset of IAM resources
Browse files Browse the repository at this point in the history
Implements support for IAM users, user policies, and access keys. This
is only a subset of what IAM can do (notably missing: roles and instance
profiles and associated policies), but it's a start.

Makes a dent in #28.
  • Loading branch information
Phil Frost committed Mar 23, 2015
1 parent 55d6824 commit d33908a
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 1 deletion.
4 changes: 4 additions & 0 deletions builtin/providers/aws/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/aws-sdk-go/gen/autoscaling"
"github.com/hashicorp/aws-sdk-go/gen/ec2"
"github.com/hashicorp/aws-sdk-go/gen/elb"
"github.com/hashicorp/aws-sdk-go/gen/iam"
"github.com/hashicorp/aws-sdk-go/gen/rds"
"github.com/hashicorp/aws-sdk-go/gen/route53"
"github.com/hashicorp/aws-sdk-go/gen/s3"
Expand All @@ -30,6 +31,7 @@ type AWSClient struct {
r53conn *route53.Route53
region string
rdsconn *rds.RDS
iamconn *iam.IAM
}

// Client configures and returns a fully initailized AWSClient
Expand Down Expand Up @@ -70,6 +72,8 @@ func (c *Config) Client() (interface{}, error) {
client.r53conn = route53.New(creds, "us-east-1", nil)
log.Println("[INFO] Initializing EC2 Connection")
client.ec2conn = ec2.New(creds, c.Region, nil)
log.Println("[INFO] Initializing IAM connection")
client.iamconn = iam.New(creds, c.Region, nil)
}

if len(errs) > 0 {
Expand Down
5 changes: 4 additions & 1 deletion builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ func Provider() terraform.ResourceProvider {
"aws_db_subnet_group": resourceAwsDbSubnetGroup(),
"aws_eip": resourceAwsEip(),
"aws_elb": resourceAwsElb(),
"aws_iam_access_key": resourceAwsIamAccessKey(),
"aws_iam_user_policy": resourceAwsIamUserPolicy(),
"aws_iam_user": resourceAwsIamUser(),
"aws_instance": resourceAwsInstance(),
"aws_internet_gateway": resourceAwsInternetGateway(),
"aws_key_pair": resourceAwsKeyPair(),
Expand All @@ -61,8 +64,8 @@ func Provider() terraform.ResourceProvider {
"aws_network_interface": resourceAwsNetworkInterface(),
"aws_route53_record": resourceAwsRoute53Record(),
"aws_route53_zone": resourceAwsRoute53Zone(),
"aws_route_table": resourceAwsRouteTable(),
"aws_route_table_association": resourceAwsRouteTableAssociation(),
"aws_route_table": resourceAwsRouteTable(),
"aws_s3_bucket": resourceAwsS3Bucket(),
"aws_security_group": resourceAwsSecurityGroup(),
"aws_subnet": resourceAwsSubnet(),
Expand Down
116 changes: 116 additions & 0 deletions builtin/providers/aws/resource_aws_iam_access_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package aws

import (
"fmt"

"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/iam"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsIamAccessKey() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIamAccessKeyCreate,
Read: resourceAwsIamAccessKeyRead,
Delete: resourceAwsIamAccessKeyDelete,

Schema: map[string]*schema.Schema{
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"status": &schema.Schema{
Type: schema.TypeString,
// this could be settable, but goamz does not support the
// UpdateAccessKey API yet.
Computed: true,
},
"secret": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}

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

request := &iam.CreateAccessKeyRequest{
UserName: aws.String(d.Get("user").(string)),
}

createResp, err := iamconn.CreateAccessKey(request)
if err != nil {
return fmt.Errorf(
"Error creating access key for user %s: %s",
request.UserName,
err,
)
}

if err := d.Set("secret", createResp.AccessKey.SecretAccessKey); err != nil {
return err
}
return resourceAwsIamAccessKeyReadResult(d, &iam.AccessKeyMetadata{
AccessKeyID: createResp.AccessKey.AccessKeyID,
CreateDate: createResp.AccessKey.CreateDate,
Status: createResp.AccessKey.Status,
UserName: createResp.AccessKey.UserName,
})
}

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

request := &iam.ListAccessKeysRequest{
UserName: aws.String(d.Get("user").(string)),
}

getResp, err := iamconn.ListAccessKeys(request)
if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX TEST ME
// the user does not exist, so the key can't exist.
d.SetId("")
return nil
}
return fmt.Errorf("Error reading IAM acces key: %s", err)
}

for _, key := range getResp.AccessKeyMetadata {
if key.AccessKeyID != nil && *key.AccessKeyID == d.Id() {
return resourceAwsIamAccessKeyReadResult(d, &key)
}
}

// Guess the key isn't around anymore.
d.SetId("")
return nil
}

func resourceAwsIamAccessKeyReadResult(d *schema.ResourceData, key *iam.AccessKeyMetadata) error {
d.SetId(*key.AccessKeyID)
if err := d.Set("user", key.UserName); err != nil {
return err
}
if err := d.Set("status", key.Status); err != nil {
return err
}
return nil
}

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

request := &iam.DeleteAccessKeyRequest{
AccessKeyID: aws.String(d.Id()),
UserName: aws.String(d.Get("user").(string)),
}

if err := iamconn.DeleteAccessKey(request); err != nil {
return fmt.Errorf("Error deleting access key %s: %s", d.Id(), err)
}
return nil
}
114 changes: 114 additions & 0 deletions builtin/providers/aws/resource_aws_iam_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package aws

import (
"fmt"

"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/iam"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsIamUser() *schema.Resource {
return &schema.Resource{
Create: resourceAwsIamUserCreate,
Read: resourceAwsIamUserRead,
// There is an UpdateUser API call, but goamz doesn't support it yet.
// XXX but we aren't using goamz anymore.
//Update: resourceAwsIamUserUpdate,
Delete: resourceAwsIamUserDelete,

Schema: map[string]*schema.Schema{
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
/*
The UniqueID could be used as the Id(), but none of the API
calls allow specifying a user by the UniqueID: they require the
name. The only way to locate a user by UniqueID is to list them
all and that would make this provider unnecessarilly complex
and inefficient. Still, there are other reasons one might want
the UniqueID, so we can make it availible.
*/
"unique_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"path": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "/",
ForceNew: true,
},
},
}
}

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

request := &iam.CreateUserRequest{
Path: aws.String(d.Get("path").(string)),
UserName: aws.String(d.Get("name").(string)),
}

createResp, err := iamconn.CreateUser(request)
if err != nil {
return fmt.Errorf("Error creating IAM User %s: %s", request.UserName, err)
}
return resourceAwsIamUserReadResult(d, createResp.User)
}

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

request := &iam.GetUserRequest{
UserName: aws.String(d.Id()),
}

getResp, err := iamconn.GetUser(request)
if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me
d.SetId("")
return nil
}
return fmt.Errorf("Error reading IAM User %s: %s", d.Id(), err)
}
return resourceAwsIamUserReadResult(d, getResp.User)
}

func resourceAwsIamUserReadResult(d *schema.ResourceData, user *iam.User) error {
d.SetId(*user.UserName)
if err := d.Set("name", user.UserName); err != nil {
return err
}
if err := d.Set("arn", user.ARN); err != nil {
return err
}
if err := d.Set("path", user.Path); err != nil {
return err
}
if err := d.Set("unique_id", user.UserID); err != nil {
return err
}
return nil
}

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

request := &iam.DeleteUserRequest{
UserName: aws.String(d.Id()),
}

if err := iamconn.DeleteUser(request); err != nil {
return fmt.Errorf("Error deleting IAM User %s: %s", d.Id(), err)
}
return nil
}
110 changes: 110 additions & 0 deletions builtin/providers/aws/resource_aws_iam_user_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package aws

import (
"fmt"
"net/url"
"strings"

"github.com/hashicorp/aws-sdk-go/aws"
"github.com/hashicorp/aws-sdk-go/gen/iam"

"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsIamUserPolicy() *schema.Resource {
return &schema.Resource{
// PutUserPolicy API is idempotent, so these can be the same.
Create: resourceAwsIamUserPolicyPut,
Update: resourceAwsIamUserPolicyPut,

Read: resourceAwsIamUserPolicyRead,
Delete: resourceAwsIamUserPolicyDelete,

Schema: map[string]*schema.Schema{
"policy": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"user": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

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

request := &iam.PutUserPolicyRequest{
UserName: aws.String(d.Get("user").(string)),
PolicyName: aws.String(d.Get("name").(string)),
PolicyDocument: aws.String(d.Get("policy").(string)),
}

if err := iamconn.PutUserPolicy(request); err != nil {
return fmt.Errorf("Error putting IAM user policy %s: %s", request.PolicyName, err)
}

d.SetId(fmt.Sprintf("%s:%s", request.UserName, request.PolicyName))
return nil
}

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

user, name := resourceAwsIamUserPolicyParseId(d)

request := &iam.GetUserPolicyRequest{
PolicyName: aws.String(name),
UserName: aws.String(user),
}

getResp, err := iamconn.GetUserPolicy(request)
if err != nil {
if iamerr, ok := err.(aws.APIError); ok && iamerr.Code == "NoSuchEntity" { // XXX test me
d.SetId("")
return nil
}
return fmt.Errorf("Error reading IAM policy %s from user %s: %s", name, user, err)
}

if getResp.PolicyDocument == nil {
return fmt.Errorf("GetUserPolicy returned a nil policy document")
}

policy, err := url.QueryUnescape(*getResp.PolicyDocument)
if err != nil {
return err
}
return d.Set("policy", policy)
}

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

user, name := resourceAwsIamUserPolicyParseId(d)

request := &iam.DeleteUserPolicyRequest{
PolicyName: aws.String(name),
UserName: aws.String(user),
}

if err := iamconn.DeleteUserPolicy(request); err != nil {
return fmt.Errorf("Error deleting IAM user policy %s: %s", d.Id(), err)
}
return nil
}

func resourceAwsIamUserPolicyParseId(d *schema.ResourceData) (userName, policyName string) {
parts := strings.SplitN(d.Id(), ":", 2)
userName = parts[0]
policyName = parts[1]
return
}
Loading

0 comments on commit d33908a

Please sign in to comment.