forked from mweagle/Sparta
/
provision_utils.go
234 lines (204 loc) · 8.16 KB
/
provision_utils.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package sparta
import (
"encoding/json"
"fmt"
"strings"
gocf "github.com/crewjam/go-cloudformation"
spartaIAM "github.com/mweagle/Sparta/aws/iam"
"github.com/mweagle/cloudformationresources"
"github.com/Sirupsen/logrus"
)
const salt = "213EA743-A98F-499D-8FEF-B87015FE13E7"
// PushSourceConfigurationActions map stores common IAM Policy Actions for Lambda
// push-source configuration management.
// The configuration is handled by CustomResources inserted into the generated
// CloudFormation template.
var PushSourceConfigurationActions = struct {
SNSLambdaEventSource []string
S3LambdaEventSource []string
SESLambdaEventSource []string
CloudWatchLogsLambdaEventSource []string
}{
SNSLambdaEventSource: []string{"sns:ConfirmSubscription",
"sns:GetTopicAttributes",
"sns:ListSubscriptionsByTopic",
"sns:Subscribe",
"sns:Unsubscribe"},
S3LambdaEventSource: []string{"s3:GetBucketLocation",
"s3:GetBucketNotification",
"s3:PutBucketNotification",
"s3:GetBucketNotificationConfiguration",
"s3:PutBucketNotificationConfiguration"},
SESLambdaEventSource: []string{"ses:CreateReceiptRuleSet",
"ses:CreateReceiptRule",
"ses:DeleteReceiptRule",
"ses:DeleteReceiptRuleSet",
"ses:DescribeReceiptRuleSet"},
CloudWatchLogsLambdaEventSource: []string{"logs:DescribeSubscriptionFilters",
"logs:DeleteSubscriptionFilter",
"logs:PutSubscriptionFilter",
},
}
func nodeJSHandlerName(jsBaseFilename string) string {
return fmt.Sprintf("index.%sConfiguration", jsBaseFilename)
}
func awsPrincipalToService(awsPrincipalName string) string {
return strings.ToUpper(strings.SplitN(awsPrincipalName, ".", 2)[0])
}
func ensureCustomResourceHandler(serviceName string,
customResourceTypeName string,
sourceArn *gocf.StringExpr,
dependsOn []string,
template *gocf.Template,
S3Bucket string,
S3Key string,
logger *logrus.Logger) (string, error) {
// AWS service basename
awsServiceName := awsPrincipalToService(customResourceTypeName)
// Use a stable resource CloudFormation resource name to represent
// the single CustomResource that can configure the different
// PushSource's for the given principal.
keyName, err := json.Marshal(ArbitraryJSONObject{
"Principal": customResourceTypeName,
"ServiceName": awsServiceName,
})
if err != nil {
logger.Error("Failed to create configurator resource name: ", err.Error())
return "", err
}
subscriberHandlerName := CloudFormationResourceName(fmt.Sprintf("%sCustomResource", awsServiceName),
string(keyName))
//////////////////////////////////////////////////////////////////////////////
// IAM Role definition
iamResourceName, err := ensureIAMRoleForCustomResource(customResourceTypeName, sourceArn, template, logger)
if nil != err {
return "", err
}
iamRoleRef := gocf.GetAtt(iamResourceName, "Arn")
_, exists := template.Resources[subscriberHandlerName]
if !exists {
logger.WithFields(logrus.Fields{
"Service": customResourceTypeName,
}).Debug("Including Lambda CustomResource for AWS Service")
configuratorDescription := customResourceDescription(serviceName, customResourceTypeName)
//////////////////////////////////////////////////////////////////////////////
// Custom Resource Lambda Handler
// The export name MUST correspond to the createForwarder entry that is dynamically
// written into the index.js file during compile in createNewSpartaCustomResourceEntry
handlerName := lambdaExportNameForCustomResourceType(customResourceTypeName)
logger.WithFields(logrus.Fields{
"CustomResourceType": customResourceTypeName,
"NodeJSExport": handlerName,
}).Debug("Sparta CloudFormation custom resource handler info")
customResourceHandlerDef := gocf.LambdaFunction{
Code: &gocf.LambdaFunctionCode{
S3Bucket: gocf.String(S3Bucket),
S3Key: gocf.String(S3Key),
},
Description: gocf.String(configuratorDescription),
Handler: gocf.String(handlerName),
Role: iamRoleRef,
Runtime: gocf.String(NodeJSVersion),
Timeout: gocf.Integer(30),
}
cfResource := template.AddResource(subscriberHandlerName, customResourceHandlerDef)
if nil != dependsOn && (len(dependsOn) > 0) {
cfResource.DependsOn = append(cfResource.DependsOn, dependsOn...)
}
}
return subscriberHandlerName, nil
}
// ensureIAMRoleForCustomResource ensures that the single IAM::Role for a single
// AWS principal (eg, s3.*.*) exists, and includes statements for the given
// sourceArn. Sparta uses a single IAM::Role for the CustomResource configuration
// lambda, which is the union of all Arns in the application.
func ensureIAMRoleForCustomResource(awsPrincipalName string,
sourceArn *gocf.StringExpr,
template *gocf.Template,
logger *logrus.Logger) (string, error) {
var principalActions []string
switch awsPrincipalName {
case cloudformationresources.SNSLambdaEventSource:
principalActions = PushSourceConfigurationActions.SNSLambdaEventSource
case cloudformationresources.S3LambdaEventSource:
principalActions = PushSourceConfigurationActions.S3LambdaEventSource
case cloudformationresources.SESLambdaEventSource:
principalActions = PushSourceConfigurationActions.SESLambdaEventSource
case cloudformationresources.CloudWatchLogsLambdaEventSource:
principalActions = PushSourceConfigurationActions.CloudWatchLogsLambdaEventSource
default:
return "", fmt.Errorf("Unsupported principal for IAM role creation: %s", awsPrincipalName)
}
// What's the stable IAMRoleName?
resourceBaseName := fmt.Sprintf("CustomResource%sIAMRole", awsPrincipalToService(awsPrincipalName))
stableRoleName := CloudFormationResourceName(resourceBaseName, awsPrincipalName)
// Ensure it exists, then check to see if this Source ARN is already specified...
// Checking equality with Stringable?
// Create a new Role
var existingIAMRole *gocf.IAMRole
existingResource, exists := template.Resources[stableRoleName]
logger.WithFields(logrus.Fields{
"PrincipalActions": principalActions,
"SourceArn": sourceArn,
}).Debug("Ensuring IAM Role results")
if !exists {
// Insert the IAM role here. We'll walk the policies data in the next section
// to make sure that the sourceARN we have is in the list
statements := CommonIAMStatements.Core
iamPolicyList := gocf.IAMPoliciesList{}
iamPolicyList = append(iamPolicyList,
gocf.IAMPolicies{
PolicyDocument: ArbitraryJSONObject{
"Version": "2012-10-17",
"Statement": statements,
},
PolicyName: gocf.String(fmt.Sprintf("%sPolicy", stableRoleName)),
},
)
existingIAMRole = &gocf.IAMRole{
AssumeRolePolicyDocument: AssumePolicyDocument,
Policies: &iamPolicyList,
}
template.AddResource(stableRoleName, existingIAMRole)
// Create a new IAM Role resource
logger.WithFields(logrus.Fields{
"RoleName": stableRoleName,
}).Debug("Inserting IAM Role")
} else {
existingIAMRole = existingResource.Properties.(*gocf.IAMRole)
}
// Walk the existing statements
if nil != existingIAMRole.Policies {
for _, eachPolicy := range *existingIAMRole.Policies {
policyDoc := eachPolicy.PolicyDocument.(ArbitraryJSONObject)
statements := policyDoc["Statement"]
for _, eachStatement := range statements.([]spartaIAM.PolicyStatement) {
if sourceArn.String() == eachStatement.Resource.String() {
logger.WithFields(logrus.Fields{
"RoleName": stableRoleName,
"SourceArn": sourceArn.String(),
}).Debug("SourceArn already exists for IAM Policy")
return stableRoleName, nil
}
}
}
logger.WithFields(logrus.Fields{
"RoleName": stableRoleName,
"Action": principalActions,
"Resource": sourceArn,
}).Debug("Inserting Actions for configuration ARN")
// Add this statement to the first policy, iff the actions are non-empty
if len(principalActions) > 0 {
rootPolicy := (*existingIAMRole.Policies)[0]
rootPolicyDoc := rootPolicy.PolicyDocument.(ArbitraryJSONObject)
rootPolicyStatements := rootPolicyDoc["Statement"].([]spartaIAM.PolicyStatement)
rootPolicyDoc["Statement"] = append(rootPolicyStatements, spartaIAM.PolicyStatement{
Effect: "Allow",
Action: principalActions,
Resource: sourceArn,
})
}
return stableRoleName, nil
}
return "", fmt.Errorf("Unable to find Policies entry for IAM role: %s", stableRoleName)
}