Skip to content

Commit

Permalink
Feature/query parameter support (Optum#108)
Browse files Browse the repository at this point in the history
* Rename DynDB tables to remove "Redbox" prefix, and "Prod" suffix

Keeping around old tables, so we can migrate data

* WIP: Almost done changing over accounts to the new router.

* WIP: Finished accounts--as in passing unit tests.

* Fixed some regressions from DCE name change.

* WIP: Adding accounts. Passing unit tests, but breaking in functional it appears from IAM-related.

* WIP: about to do second round of functional testing. Passed first time..
  • Loading branch information
nathanagood authored Nov 11, 2019
1 parent b0b918c commit 0f56673
Show file tree
Hide file tree
Showing 22 changed files with 1,050 additions and 600 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
## vNext

- Added `/accounts?accountStatus=<status>` URL for querying accounts by status.
- Added Lease Validation for check against max budget amount, max budget period, principal budget amount and principal budget period
- Increase the threshold for Reset CodeBuild alarms to 10 failures over 5 hours.
\

## v0.22.0

**BREAKING CHANGES**
Expand Down
205 changes: 150 additions & 55 deletions cmd/lambda/accounts/create.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -10,77 +9,93 @@ import (
"strings"
"time"

"github.com/Optum/dce/pkg/rolemanager"
"github.com/aws/aws-sdk-go/service/iam"

"github.com/Optum/dce/pkg/api/response"
"github.com/Optum/dce/pkg/common"
"github.com/Optum/dce/pkg/db"
"github.com/aws/aws-lambda-go/events"
"github.com/Optum/dce/pkg/rolemanager"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
)

type createController struct {
Dao db.DBer
Queue common.Queue
ResetQueueURL string
SNS common.Notificationer
AccountCreatedTopicArn string
AWSSession session.Session
TokenService common.TokenService
StoragerService common.Storager
RoleManager rolemanager.RoleManager
PrincipalRoleName string
PrincipalPolicyName string
PrincipalMaxSessionDuration int64
// The IAM Principal role will be denied access
// to resources with these tags leased
PrincipalIAMDenyTags []string
// Tags to apply to AWS resources created by this controller
Tags []*iam.Tag
ArtifactsBucket string
PrincipalPolicyS3Key string
var (
accountCreatedTopicArn string
policyName string
artifactsBucket string
principalPolicyS3Key string
principalRoleName string
principalIAMDenyTags []string
principalMaxSessionDuration int64
tags []*iam.Tag
resetQueueURL string
)

func init() {
policyName = Config.GetEnvVar("PRINCIPAL_POLICY_NAME", "DCEPrincipalDefaultPolicy")
artifactsBucket = Config.GetEnvVar("ARTIFACTS_BUCKET", "DefaultArtifactBucket")
principalPolicyS3Key = Config.GetEnvVar("PRINCIPAL_POLICY_S3_KEY", "DefaultPrincipalPolicyS3Key")
principalRoleName = Config.GetEnvVar("PRINCIPAL_ROLE_NAME", "DCEPrincipal")
principalIAMDenyTags = strings.Split(Config.GetEnvVar("PRINCIPAL_IAM_DENY_TAGS", "DefaultPrincipalIamDenyTags"), ",")
principalMaxSessionDuration = int64(Config.GetEnvIntVar("PRINCIPAL_MAX_SESSION_DURATION", 100))
tags = []*iam.Tag{
{Key: aws.String("Terraform"), Value: aws.String("False")},
{Key: aws.String("Source"), Value: aws.String("github.com/Optum/dce//cmd/lambda/accounts")},
{Key: aws.String("Environment"), Value: aws.String(Config.GetEnvVar("TAG_ENVIRONMENT", "DefaultTagEnvironment"))},
{Key: aws.String("Contact"), Value: aws.String(Config.GetEnvVar("TAG_CONTACT", "DefaultTagContact"))},
{Key: aws.String("AppName"), Value: aws.String(Config.GetEnvVar("TAG_APP_NAME", "DefaultTagAppName"))},
}
accountCreatedTopicArn = Config.GetEnvVar("ACCOUNT_CREATED_TOPIC_ARN", "DefaultAccountCreatedTopicArn")
resetQueueURL = Config.GetEnvVar("RESET_SQS_URL", "DefaultResetSQSUrl")
}

// Call - Function to validate the account request to add into the pool and
// CreateAccount - Function to validate the account request to add into the pool and
// publish the account creation to its respective client
func (c createController) Call(ctx context.Context, req *events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
func CreateAccount(w http.ResponseWriter, r *http.Request) {

// Marshal the request JSON into a CreateRequest object
var request createRequest
err := json.Unmarshal([]byte(req.Body), &request)
request := &CreateRequest{}
var err error
decoder := json.NewDecoder(r.Body)
err = decoder.Decode(&request)

if err != nil {
return response.RequestValidationError("invalid request parameters"), nil
WriteAPIErrorResponse(w, http.StatusBadRequest, "ClientError", "invalid request parameters")
return
}

// Validate the request body
isValid, validationRes := request.Validate()
if !isValid {
return *validationRes, nil
WriteAPIErrorResponse(w, http.StatusBadRequest, "ClientError", *validationRes)
return
}

// Check if the account already exists
existingAccount, err := c.Dao.GetAccount(request.ID)
existingAccount, err := Dao.GetAccount(request.ID)
if err != nil {
log.Printf("Failed to add account %s to pool: %s",
request.ID, err.Error())
return response.ServerError(), nil
WriteAPIErrorResponse(w, http.StatusInternalServerError, "ServerError", "")
return
}
if existingAccount != nil {
return response.AlreadyExistsError(), nil
WriteAlreadyExistsError(w)
return
}

// Verify that we can assume role in the account,
// using the `adminRoleArn`
_, err = c.TokenService.AssumeRole(&sts.AssumeRoleInput{
_, err = TokenSvc.AssumeRole(&sts.AssumeRoleInput{
RoleArn: aws.String(request.AdminRoleArn),
RoleSessionName: aws.String("DCEAssumeRoleVerification"),
RoleSessionName: aws.String("MasterAssumeRoleVerification"),
})

if err != nil {
return response.RequestValidationError(
WriteRequestValidationError(
w,
fmt.Sprintf("Unable to add account %s to pool: adminRole is not assumable by the master account", request.ID),
), nil
)
return
}

// Prepare the account record
Expand All @@ -94,56 +109,68 @@ func (c createController) Call(ctx context.Context, req *events.APIGatewayProxyR
}

// Create an IAM Role for the principal (end-user) to login to
createRolRes, policyHash, err := c.createPrincipalRole(account)
createRolRes, policyHash, err := createPrincipalRole(account)
if err != nil {
log.Printf("failed to create principal role for %s: %s", request.ID, err)
return response.ServerError(), nil
WriteServerErrorWithResponse(w, "Internal server error")
return
}
account.PrincipalRoleArn = createRolRes.RoleArn
account.PrincipalPolicyHash = policyHash

// Write the Account to the DB
err = c.Dao.PutAccount(account)
err = Dao.PutAccount(account)
if err != nil {
log.Printf("Failed to add account %s to pool: %s",
request.ID, err.Error())
return response.ServerError(), nil
WriteServerErrorWithResponse(w, "Internal server error")
return
}

// Add Account to Reset Queue
err = c.Queue.SendMessage(&c.ResetQueueURL, &account.ID)
err = Queue.SendMessage(&resetQueueURL, &account.ID)
if err != nil {
log.Printf("Failed to add account %s to reset Queue: %s", account.ID, err)
return response.ServerError(), nil
WriteServerErrorWithResponse(w, "Internal server error")
return
}

// Publish the Account to an "account-created" topic
accountResponse := response.AccountResponse(account)
snsMessage, err := common.PrepareSNSMessageJSON(accountResponse)
if err != nil {
log.Printf("Failed to create SNS account-created message for %s: %s", account.ID, err)
return response.ServerError(), nil
WriteServerErrorWithResponse(w, "Internal server error")
return
}
_, err = c.SNS.PublishMessage(&c.AccountCreatedTopicArn, &snsMessage, true)

// TODO: Initialize these in a better spot.

_, err = SnsSvc.PublishMessage(&accountCreatedTopicArn, &snsMessage, true)
if err != nil {
log.Printf("Failed to publish SNS account-created message for %s: %s", account.ID, err)
return response.ServerError(), nil
WriteServerErrorWithResponse(w, "Internal server error")
return
}

return response.CreateJSONResponse(
accountResponseJSON, err := json.Marshal(accountResponse)

WriteAPIResponse(
w,
http.StatusCreated,
accountResponse,
), nil
string(accountResponseJSON),
)
}

type createRequest struct {
// CreateRequest - The create account request
type CreateRequest struct {
ID string `json:"id"`
AdminRoleArn string `json:"adminRoleArn"`
}

// Validate - Checks if the Account Request has the provided id and adminRoleArn
// fields
func (req *createRequest) Validate() (bool, *events.APIGatewayProxyResponse) {
func (req *CreateRequest) Validate() (bool, *string) {
isValid := true
var validationErrors []error
if req.ID == "" {
Expand All @@ -161,9 +188,77 @@ func (req *createRequest) Validate() (bool, *events.APIGatewayProxyResponse) {
errMsgs = append(errMsgs, verr.Error())
}
msg := strings.Join(errMsgs, "; ")
res := response.RequestValidationError(msg)
return false, &res
return false, &msg
}

return true, nil
}

func createPrincipalRole(account db.Account) (*rolemanager.CreateRoleWithPolicyOutput, string, error) {
// Create an assume role policy,
// to let principals from the same account assume the role.
//
// Consumers of open source DCE may modify and customize
// this as need (eg. to integrate with SSO/SAML)
// by responding to the "account-created" SNS topic
assumeRolePolicy := strings.TrimSpace(fmt.Sprintf(`
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::%s:root"
},
"Action": "sts:AssumeRole",
"Condition": {}
}
]
}
`, account.ID))

// Render the default policy for the principal

policy, policyHash, err := StorageSvc.GetTemplateObject(artifactsBucket, principalPolicyS3Key,
principalPolicyInput{
PrincipalPolicyArn: fmt.Sprintf("arn:aws:iam::%s:policy/%s", account.ID, policyName),
PrincipalRoleArn: fmt.Sprintf("arn:aws:iam::%s:role/%s", account.ID, principalRoleName),
PrincipalIAMDenyTags: principalIAMDenyTags,
AdminRoleArn: account.AdminRoleArn,
})
if err != nil {
return nil, "", err
}

// Assume role into the new account
accountSession, err := TokenSvc.NewSession(AWSSession, account.AdminRoleArn)
if err != nil {
return nil, "", err
}
iamClient := iam.New(accountSession)

// Create the Role + Policy
RoleManager.SetIAMClient(iamClient)
createRoleOutput := &rolemanager.CreateRoleWithPolicyOutput{}
createRoleOutput, err = RoleManager.CreateRoleWithPolicy(&rolemanager.CreateRoleWithPolicyInput{
RoleName: principalRoleName,
RoleDescription: "Role to be assumed by principal users of DCE",
AssumeRolePolicyDocument: assumeRolePolicy,
MaxSessionDuration: principalMaxSessionDuration,
PolicyName: policyName,
PolicyDocument: policy,
PolicyDescription: "Policy for principal users of DCE",
Tags: append(tags,
&iam.Tag{Key: aws.String("Name"), Value: aws.String("DCEPrincipal")},
),
IgnoreAlreadyExistsErrors: true,
})
return createRoleOutput, policyHash, err
}

type principalPolicyInput struct {
PrincipalPolicyArn string
PrincipalRoleArn string
PrincipalIAMDenyTags []string
AdminRoleArn string
}
Loading

0 comments on commit 0f56673

Please sign in to comment.