github.com/mweagle/Sparta@v1.15.0/lambda_permissions.go (about)

     1  package sparta
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/aws/aws-sdk-go/service/s3"
     9  	spartaCF "github.com/mweagle/Sparta/aws/cloudformation"
    10  	cfCustomResources "github.com/mweagle/Sparta/aws/cloudformation/resources"
    11  	gocf "github.com/mweagle/go-cloudformation"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  )
    15  
    16  ////////////////////////////////////////////////////////////////////////////////
    17  // Types to handle permissions & push source configuration
    18  type descriptionNode struct {
    19  	Name     string
    20  	Relation string
    21  	Color    string
    22  }
    23  
    24  // LambdaPermissionExporter defines an interface for polymorphic collection of
    25  // Permission entries that support specialization for additional resource generation.
    26  type LambdaPermissionExporter interface {
    27  	// Export the permission object to a set of CloudFormation resources
    28  	// in the provided resources param.  The targetLambdaFuncRef
    29  	// interface represents the Fn::GetAtt "Arn" JSON value
    30  	// of the parent Lambda target
    31  	export(serviceName string,
    32  		lambdaFunctionDisplayName string,
    33  		lambdaLogicalCFResourceName string,
    34  		template *gocf.Template,
    35  		S3Bucket string,
    36  		S3Key string,
    37  		logger *logrus.Logger) (string, error)
    38  	// Return a `describe` compatible output for the given permission.  Return
    39  	// value is a list of tuples for node, edgeLabel
    40  	descriptionInfo() ([]descriptionNode, error)
    41  }
    42  
    43  ////////////////////////////////////////////////////////////////////////////////
    44  // START - BasePermission
    45  //
    46  
    47  // BasePermission (http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-permission.html)
    48  // type for common AWS Lambda permission data.
    49  type BasePermission struct {
    50  	// The AWS account ID (without hyphens) of the source owner
    51  	SourceAccount string `json:"SourceAccount,omitempty"`
    52  	// The ARN of a resource that is invoking your function.
    53  	SourceArn interface{} `json:"SourceArn,omitempty"`
    54  }
    55  
    56  func (perm *BasePermission) sourceArnExpr(joinParts ...gocf.Stringable) *gocf.StringExpr {
    57  	if perm.SourceArn == nil {
    58  		return nil
    59  	}
    60  	stringARN, stringARNOk := perm.SourceArn.(string)
    61  	if stringARNOk && strings.Contains(stringARN, "arn:aws:") {
    62  		return gocf.String(stringARN)
    63  	}
    64  
    65  	var parts []gocf.Stringable
    66  	if nil != joinParts {
    67  		parts = append(parts, joinParts...)
    68  	}
    69  	parts = append(parts,
    70  		spartaCF.DynamicValueToStringExpr(perm.SourceArn),
    71  	)
    72  	return gocf.Join("", parts...)
    73  }
    74  
    75  func (perm BasePermission) export(principal *gocf.StringExpr,
    76  	arnPrefixParts []gocf.Stringable,
    77  	lambdaFunctionDisplayName string,
    78  	lambdaLogicalCFResourceName string,
    79  	template *gocf.Template,
    80  	S3Bucket string,
    81  	S3Key string,
    82  	logger *logrus.Logger) (string, error) {
    83  
    84  	lambdaPermission := gocf.LambdaPermission{
    85  		Action:       gocf.String("lambda:InvokeFunction"),
    86  		FunctionName: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"),
    87  		Principal:    principal,
    88  	}
    89  	// If the Arn isn't the wildcard value, then include it.
    90  	if nil != perm.SourceArn {
    91  		switch typedARN := perm.SourceArn.(type) {
    92  		case string:
    93  			// Don't be smart if the Arn value is a user supplied literal
    94  			if typedARN != "*" {
    95  				lambdaPermission.SourceArn = gocf.String(typedARN)
    96  			}
    97  		default:
    98  			lambdaPermission.SourceArn = perm.sourceArnExpr(arnPrefixParts...)
    99  		}
   100  	}
   101  
   102  	if perm.SourceAccount != "" {
   103  		lambdaPermission.SourceAccount = gocf.String(perm.SourceAccount)
   104  	}
   105  
   106  	arnLiteral, arnLiteralErr := json.Marshal(lambdaPermission.SourceArn)
   107  	if nil != arnLiteralErr {
   108  		return "", arnLiteralErr
   109  	}
   110  	resourceName := CloudFormationResourceName("LambdaPerm%s",
   111  		principal.Literal,
   112  		string(arnLiteral),
   113  		lambdaLogicalCFResourceName)
   114  	template.AddResource(resourceName, lambdaPermission)
   115  	return resourceName, nil
   116  }
   117  
   118  //
   119  // END - BasePermission
   120  ////////////////////////////////////////////////////////////////////////////////
   121  
   122  ////////////////////////////////////////////////////////////////////////////////
   123  // START - S3Permission
   124  //
   125  var s3SourceArnParts = []gocf.Stringable{
   126  	gocf.String("arn:aws:s3:::"),
   127  }
   128  
   129  // S3Permission struct implies that the S3 BasePermission.SourceArn should be
   130  // updated (via PutBucketNotificationConfiguration) to automatically push
   131  // events to the owning Lambda.
   132  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
   133  // for more information.
   134  type S3Permission struct {
   135  	BasePermission
   136  	// S3 events to register for (eg: `[]string{s3:GetObjectObjectCreated:*", "s3:ObjectRemoved:*"}`).
   137  	Events []string `json:"Events,omitempty"`
   138  	// S3.NotificationConfigurationFilter
   139  	// to scope event forwarding.  See
   140  	// 		http://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
   141  	// for more information.
   142  	Filter s3.NotificationConfigurationFilter `json:"Filter,omitempty"`
   143  }
   144  
   145  func (perm S3Permission) export(serviceName string,
   146  	lambdaFunctionDisplayName string,
   147  	lambdaLogicalCFResourceName string,
   148  	template *gocf.Template,
   149  	S3Bucket string,
   150  	S3Key string,
   151  	logger *logrus.Logger) (string, error) {
   152  
   153  	targetLambdaResourceName, err := perm.BasePermission.export(gocf.String("s3.amazonaws.com"),
   154  		s3SourceArnParts,
   155  		lambdaFunctionDisplayName,
   156  		lambdaLogicalCFResourceName,
   157  		template,
   158  		S3Bucket,
   159  		S3Key,
   160  		logger)
   161  
   162  	if nil != err {
   163  		return "", errors.Wrap(err, "Failed to export S3 permission")
   164  	}
   165  
   166  	// Make sure the custom lambda that manages s3 notifications is provisioned.
   167  	sourceArnExpression := perm.BasePermission.sourceArnExpr(s3SourceArnParts...)
   168  	configuratorResName, err := EnsureCustomResourceHandler(serviceName,
   169  		cfCustomResources.S3LambdaEventSource,
   170  		sourceArnExpression,
   171  		[]string{},
   172  		template,
   173  		S3Bucket,
   174  		S3Key,
   175  		logger)
   176  
   177  	if nil != err {
   178  		return "", errors.Wrap(err, "Exporting S3 permission")
   179  	}
   180  
   181  	// Add a custom resource invocation for this configuration
   182  	//////////////////////////////////////////////////////////////////////////////
   183  	newResource, newResourceError := newCloudFormationResource(cfCustomResources.S3LambdaEventSource,
   184  		logger)
   185  	if nil != newResourceError {
   186  		return "", newResourceError
   187  	}
   188  	// Setup the reqest for the S3 action
   189  	s3Resource, s3ResourceOK := newResource.(*cfCustomResources.S3LambdaEventSourceResource)
   190  	if !s3ResourceOK {
   191  		return "", fmt.Errorf("failed to access typed S3CustomResource")
   192  	}
   193  	s3Resource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn")
   194  	s3Resource.BucketArn = sourceArnExpression
   195  	s3Resource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn")
   196  	s3Resource.Events = perm.Events
   197  	if nil != perm.Filter.Key {
   198  		s3Resource.Filter = &perm.Filter
   199  	}
   200  
   201  	// Name?
   202  	resourceInvokerName := CloudFormationResourceName("ConfigS3",
   203  		lambdaLogicalCFResourceName,
   204  		perm.BasePermission.SourceAccount,
   205  		fmt.Sprintf("%#v", s3Resource.Filter))
   206  
   207  	// Add it
   208  	cfResource := template.AddResource(resourceInvokerName, s3Resource)
   209  	cfResource.DependsOn = append(cfResource.DependsOn,
   210  		targetLambdaResourceName,
   211  		configuratorResName)
   212  	return "", nil
   213  }
   214  
   215  func (perm S3Permission) descriptionInfo() ([]descriptionNode, error) {
   216  	s3Events := ""
   217  	for _, eachEvent := range perm.Events {
   218  		s3Events = fmt.Sprintf("%s\n%s", eachEvent, s3Events)
   219  	}
   220  	nodes := make([]descriptionNode, 0)
   221  	if perm.Filter.Key == nil || len(perm.Filter.Key.FilterRules) == 0 {
   222  		nodes = append(nodes, descriptionNode{
   223  			Name:     describeInfoValue(perm.SourceArn),
   224  			Relation: s3Events,
   225  		})
   226  	} else {
   227  		for _, eachFilter := range perm.Filter.Key.FilterRules {
   228  			filterRel := fmt.Sprintf("%s (%s = %s)",
   229  				s3Events,
   230  				*eachFilter.Name,
   231  				*eachFilter.Value)
   232  			nodes = append(nodes, descriptionNode{
   233  				Name:     describeInfoValue(perm.SourceArn),
   234  				Relation: filterRel,
   235  			})
   236  		}
   237  	}
   238  
   239  	return nodes, nil
   240  }
   241  
   242  // END - S3Permission
   243  ///////////////////////////////////////////////////////////////////////////////////
   244  
   245  ////////////////////////////////////////////////////////////////////////////////
   246  // SNSPermission - START
   247  var snsSourceArnParts = []gocf.Stringable{}
   248  
   249  // SNSPermission struct implies that the BasePermisison.SourceArn should be
   250  // configured for subscriptions as part of this stacks provisioning.
   251  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
   252  // for more information.
   253  type SNSPermission struct {
   254  	BasePermission
   255  }
   256  
   257  func (perm SNSPermission) export(serviceName string,
   258  	lambdaFunctionDisplayName string,
   259  	lambdaLogicalCFResourceName string,
   260  	template *gocf.Template,
   261  	S3Bucket string,
   262  	S3Key string,
   263  	logger *logrus.Logger) (string, error) {
   264  	sourceArnExpression := perm.BasePermission.sourceArnExpr(snsSourceArnParts...)
   265  
   266  	targetLambdaResourceName, err := perm.BasePermission.export(gocf.String(SNSPrincipal),
   267  		snsSourceArnParts,
   268  		lambdaFunctionDisplayName,
   269  		lambdaLogicalCFResourceName,
   270  		template,
   271  		S3Bucket,
   272  		S3Key,
   273  		logger)
   274  	if nil != err {
   275  		return "", errors.Wrap(err, "Failed to export SNS permission")
   276  	}
   277  
   278  	// Make sure the custom lambda that manages s3 notifications is provisioned.
   279  	configuratorResName, err := EnsureCustomResourceHandler(serviceName,
   280  		cfCustomResources.SNSLambdaEventSource,
   281  		sourceArnExpression,
   282  		[]string{},
   283  		template,
   284  		S3Bucket,
   285  		S3Key,
   286  		logger)
   287  
   288  	if nil != err {
   289  		return "", errors.Wrap(err, "Exporing SNS permission handler")
   290  	}
   291  
   292  	// Add a custom resource invocation for this configuration
   293  	//////////////////////////////////////////////////////////////////////////////
   294  	newResource, newResourceError := newCloudFormationResource(cfCustomResources.SNSLambdaEventSource,
   295  		logger)
   296  	if nil != newResourceError {
   297  		return "", newResourceError
   298  	}
   299  	customResource := newResource.(*cfCustomResources.SNSLambdaEventSourceResource)
   300  	customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn")
   301  	customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn")
   302  	customResource.SNSTopicArn = sourceArnExpression
   303  
   304  	// Name?
   305  	resourceInvokerName := CloudFormationResourceName("ConfigSNS",
   306  		lambdaLogicalCFResourceName,
   307  		perm.BasePermission.SourceAccount)
   308  
   309  	// Add it
   310  	cfResource := template.AddResource(resourceInvokerName, customResource)
   311  	cfResource.DependsOn = append(cfResource.DependsOn,
   312  		targetLambdaResourceName,
   313  		configuratorResName)
   314  	return "", nil
   315  }
   316  
   317  func (perm SNSPermission) descriptionInfo() ([]descriptionNode, error) {
   318  	nodes := []descriptionNode{
   319  		{
   320  			Name:     describeInfoValue(perm.SourceArn),
   321  			Relation: "",
   322  		},
   323  	}
   324  	return nodes, nil
   325  }
   326  
   327  //
   328  // END - SNSPermission
   329  ////////////////////////////////////////////////////////////////////////////////
   330  
   331  ////////////////////////////////////////////////////////////////////////////////
   332  // MessageBodyStorageOptions - START
   333  
   334  // MessageBodyStorageOptions define additional options for storing SES
   335  // message body content.  By default, all rules associated with the owning
   336  // SESPermission object will store message bodies if the MessageBodyStorage
   337  // field is non-nil.  Message bodies are by default prefixed with
   338  // `ServiceName/RuleName/`, which can be overridden by specifying a non-empty
   339  // ObjectKeyPrefix value.  A rule can opt-out of message body storage
   340  // with the DisableStorage field.  See
   341  // http://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-s3.html
   342  // for additional field documentation.
   343  // The message body is saved as MIME (https://tools.ietf.org/html/rfc2045)
   344  type MessageBodyStorageOptions struct {
   345  	ObjectKeyPrefix string
   346  	KmsKeyArn       string
   347  	TopicArn        string
   348  	DisableStorage  bool
   349  }
   350  
   351  //
   352  // END - MessageBodyStorageOptions
   353  ////////////////////////////////////////////////////////////////////////////////
   354  
   355  ////////////////////////////////////////////////////////////////////////////////
   356  // MessageBodyStorage - START
   357  
   358  // MessageBodyStorage represents either a new S3 bucket or an existing S3 bucket
   359  // to which SES message bodies should be stored.
   360  // NOTE: New MessageBodyStorage create S3 buckets which will be orphaned after your
   361  // service is deleted.
   362  type MessageBodyStorage struct {
   363  	logicalBucketName                  string
   364  	bucketNameExpr                     *gocf.StringExpr
   365  	cloudFormationS3BucketResourceName string
   366  }
   367  
   368  // BucketArn returns an Arn value that can be used as an
   369  // lambdaFn.RoleDefinition.Privileges `Resource` value.
   370  func (storage *MessageBodyStorage) BucketArn() *gocf.StringExpr {
   371  	return gocf.Join("",
   372  		gocf.String("arn:aws:s3:::"),
   373  		storage.bucketNameExpr)
   374  }
   375  
   376  // BucketArnAllKeys returns an Arn value that can be used
   377  // lambdaFn.RoleDefinition.Privileges `Resource` value.  It includes
   378  // the trailing `/*` wildcard to support item acccess
   379  func (storage *MessageBodyStorage) BucketArnAllKeys() *gocf.StringExpr {
   380  	return gocf.Join("",
   381  		gocf.String("arn:aws:s3:::"),
   382  		storage.bucketNameExpr,
   383  		gocf.String("/*"))
   384  }
   385  
   386  func (storage *MessageBodyStorage) export(serviceName string,
   387  	lambdaFunctionDisplayName string,
   388  	lambdaLogicalCFResourceName string,
   389  	template *gocf.Template,
   390  	S3Bucket string,
   391  	S3Key string,
   392  	logger *logrus.Logger) (string, error) {
   393  
   394  	if storage.cloudFormationS3BucketResourceName != "" {
   395  		s3Bucket := &gocf.S3Bucket{
   396  			Tags: &gocf.TagList{
   397  				gocf.Tag{
   398  					Key:   gocf.String("sparta:logicalBucketName"),
   399  					Value: gocf.String(storage.logicalBucketName),
   400  				},
   401  			},
   402  		}
   403  		cfResource := template.AddResource(storage.cloudFormationS3BucketResourceName, s3Bucket)
   404  		cfResource.DeletionPolicy = "Retain"
   405  
   406  		lambdaResource, lambdaResourceExists := template.Resources[lambdaLogicalCFResourceName]
   407  		if !lambdaResourceExists {
   408  			safeAppendDependency(lambdaResource, storage.cloudFormationS3BucketResourceName)
   409  		}
   410  
   411  		logger.WithFields(logrus.Fields{
   412  			"LogicalResourceName": storage.cloudFormationS3BucketResourceName,
   413  		}).Info("Service will orphan S3 Bucket on deletion")
   414  
   415  		// Save the output
   416  		template.Outputs[storage.cloudFormationS3BucketResourceName] = &gocf.Output{
   417  			Description: "SES Message Body Bucket",
   418  			Value:       gocf.Ref(storage.cloudFormationS3BucketResourceName),
   419  		}
   420  	}
   421  	// Add the S3 Access policy
   422  	s3BodyStoragePolicy := &gocf.S3BucketPolicy{
   423  		Bucket: storage.bucketNameExpr,
   424  		PolicyDocument: ArbitraryJSONObject{
   425  			"Version": "2012-10-17",
   426  			"Statement": []ArbitraryJSONObject{
   427  				{
   428  					"Sid":    "PermitSESServiceToSaveEmailBody",
   429  					"Effect": "Allow",
   430  					"Principal": ArbitraryJSONObject{
   431  						"Service": "ses.amazonaws.com",
   432  					},
   433  					"Action": []string{"s3:PutObjectAcl", "s3:PutObject"},
   434  					"Resource": gocf.Join("",
   435  						gocf.String("arn:aws:s3:::"),
   436  						storage.bucketNameExpr,
   437  						gocf.String("/*")),
   438  					"Condition": ArbitraryJSONObject{
   439  						"StringEquals": ArbitraryJSONObject{
   440  							"aws:Referer": gocf.Ref("AWS::AccountId"),
   441  						},
   442  					},
   443  				},
   444  			},
   445  		},
   446  	}
   447  
   448  	s3BucketPolicyResourceName := CloudFormationResourceName("SESMessageBodyBucketPolicy",
   449  		fmt.Sprintf("%#v", storage.bucketNameExpr))
   450  	template.AddResource(s3BucketPolicyResourceName, s3BodyStoragePolicy)
   451  
   452  	// Return the name of the bucket policy s.t. the configurator resource
   453  	// is properly sequenced.  The configurator will fail iff the Bucket Policies aren't
   454  	// applied b/c the SES Rule Actions check PutObject access to S3 buckets
   455  	return s3BucketPolicyResourceName, nil
   456  }
   457  
   458  // Return a function that
   459  
   460  //
   461  // END - MessageBodyStorage
   462  ////////////////////////////////////////////////////////////////////////////////
   463  
   464  ////////////////////////////////////////////////////////////////////////////////
   465  // ReceiptRule - START
   466  
   467  // ReceiptRule represents an SES ReceiptRule
   468  // (http://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-receipt-rules.html)
   469  // value.  To store message bodies, provide a non-nil MessageBodyStorage value
   470  // to the owning SESPermission object
   471  type ReceiptRule struct {
   472  	Name               string
   473  	Disabled           bool
   474  	Recipients         []string
   475  	ScanDisabled       bool
   476  	TLSPolicy          string
   477  	TopicArn           string
   478  	InvocationType     string
   479  	BodyStorageOptions MessageBodyStorageOptions
   480  }
   481  
   482  func (rule *ReceiptRule) toResourceRule(serviceName string,
   483  	functionArnRef interface{},
   484  	messageBodyStorage *MessageBodyStorage) *cfCustomResources.SESLambdaEventSourceResourceRule {
   485  
   486  	resourceRule := &cfCustomResources.SESLambdaEventSourceResourceRule{
   487  		Name:        gocf.String(rule.Name),
   488  		ScanEnabled: gocf.Bool(!rule.ScanDisabled),
   489  		Enabled:     gocf.Bool(!rule.Disabled),
   490  		Actions:     make([]*cfCustomResources.SESLambdaEventSourceResourceAction, 0),
   491  		Recipients:  make([]*gocf.StringExpr, 0),
   492  	}
   493  	for _, eachRecipient := range rule.Recipients {
   494  		resourceRule.Recipients = append(resourceRule.Recipients, gocf.String(eachRecipient))
   495  	}
   496  	if rule.TLSPolicy != "" {
   497  		resourceRule.TLSPolicy = gocf.String(rule.TLSPolicy)
   498  	}
   499  
   500  	// If there is a MessageBodyStorage reference, push that S3Action
   501  	// to the head of the Actions list
   502  	if nil != messageBodyStorage && !rule.BodyStorageOptions.DisableStorage {
   503  		s3Action := &cfCustomResources.SESLambdaEventSourceResourceAction{
   504  			ActionType: gocf.String("S3Action"),
   505  			ActionProperties: map[string]interface{}{
   506  				"BucketName": messageBodyStorage.bucketNameExpr,
   507  			},
   508  		}
   509  		if rule.BodyStorageOptions.ObjectKeyPrefix != "" {
   510  			s3Action.ActionProperties["ObjectKeyPrefix"] = rule.BodyStorageOptions.ObjectKeyPrefix
   511  		}
   512  		if rule.BodyStorageOptions.KmsKeyArn != "" {
   513  			s3Action.ActionProperties["KmsKeyArn"] = rule.BodyStorageOptions.KmsKeyArn
   514  		}
   515  		if rule.BodyStorageOptions.TopicArn != "" {
   516  			s3Action.ActionProperties["TopicArn"] = rule.BodyStorageOptions.TopicArn
   517  		}
   518  		resourceRule.Actions = append(resourceRule.Actions, s3Action)
   519  	}
   520  	// There's always a lambda action
   521  	lambdaAction := &cfCustomResources.SESLambdaEventSourceResourceAction{
   522  		ActionType: gocf.String("LambdaAction"),
   523  		ActionProperties: map[string]interface{}{
   524  			"FunctionArn": functionArnRef,
   525  		},
   526  	}
   527  	lambdaAction.ActionProperties["InvocationType"] = rule.InvocationType
   528  	if rule.InvocationType == "" {
   529  		lambdaAction.ActionProperties["InvocationType"] = "Event"
   530  	}
   531  	if rule.TopicArn != "" {
   532  		lambdaAction.ActionProperties["TopicArn"] = rule.TopicArn
   533  	}
   534  	resourceRule.Actions = append(resourceRule.Actions, lambdaAction)
   535  	return resourceRule
   536  }
   537  
   538  //
   539  // END - ReceiptRule
   540  ////////////////////////////////////////////////////////////////////////////////
   541  
   542  ////////////////////////////////////////////////////////////////////////////////
   543  // SESPermission - START
   544  
   545  // SES doesn't use ARNs to scope access
   546  var sesSourcePartArn = []gocf.Stringable{wildcardArn}
   547  
   548  // SESPermission struct implies that the SES verified domain should be
   549  // updated (via createReceiptRule) to automatically request or push events
   550  // to the parent lambda
   551  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
   552  // for more information.  See http://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-concepts.html
   553  // for setting up email receiving.
   554  type SESPermission struct {
   555  	BasePermission
   556  	InvocationType     string /* RequestResponse, Event */
   557  	ReceiptRules       []ReceiptRule
   558  	MessageBodyStorage *MessageBodyStorage
   559  }
   560  
   561  // NewMessageBodyStorageResource provisions a new S3 bucket to store message body
   562  // content.
   563  func (perm *SESPermission) NewMessageBodyStorageResource(bucketLogicalName string) (*MessageBodyStorage, error) {
   564  	if len(bucketLogicalName) <= 0 {
   565  		return nil, errors.New("NewMessageBodyStorageResource requires a unique, non-empty `bucketLogicalName` parameter ")
   566  	}
   567  	store := &MessageBodyStorage{
   568  		logicalBucketName: bucketLogicalName,
   569  	}
   570  	store.cloudFormationS3BucketResourceName = CloudFormationResourceName("SESMessageStoreBucket", bucketLogicalName)
   571  	store.bucketNameExpr = gocf.Ref(store.cloudFormationS3BucketResourceName).String()
   572  	return store, nil
   573  }
   574  
   575  // NewMessageBodyStorageReference uses a pre-existing S3 bucket for MessageBody storage.
   576  // Sparta assumes that prexistingBucketName exists and will add an S3::BucketPolicy
   577  // to enable SES PutObject access.
   578  func (perm *SESPermission) NewMessageBodyStorageReference(prexistingBucketName string) (*MessageBodyStorage, error) {
   579  	store := &MessageBodyStorage{}
   580  	store.bucketNameExpr = gocf.String(prexistingBucketName)
   581  	return store, nil
   582  }
   583  
   584  func (perm SESPermission) export(serviceName string,
   585  	lambdaFunctionDisplayName string,
   586  	lambdaLogicalCFResourceName string,
   587  	template *gocf.Template,
   588  	S3Bucket string,
   589  	S3Key string,
   590  	logger *logrus.Logger) (string, error) {
   591  
   592  	sourceArnExpression := perm.BasePermission.sourceArnExpr(snsSourceArnParts...)
   593  
   594  	targetLambdaResourceName, err := perm.BasePermission.export(gocf.String(SESPrincipal),
   595  		sesSourcePartArn,
   596  		lambdaFunctionDisplayName,
   597  		lambdaLogicalCFResourceName,
   598  		template,
   599  		S3Bucket,
   600  		S3Key,
   601  		logger)
   602  	if nil != err {
   603  		return "", errors.Wrap(err, "Failed to export SES permission")
   604  	}
   605  
   606  	// MessageBody storage?
   607  	var dependsOn []string
   608  	if nil != perm.MessageBodyStorage {
   609  		s3Policy, s3PolicyErr := perm.MessageBodyStorage.export(serviceName,
   610  			lambdaFunctionDisplayName,
   611  			lambdaLogicalCFResourceName,
   612  			template,
   613  			S3Bucket,
   614  			S3Key,
   615  			logger)
   616  		if nil != s3PolicyErr {
   617  			return "", s3PolicyErr
   618  		}
   619  		if s3Policy != "" {
   620  			dependsOn = append(dependsOn, s3Policy)
   621  		}
   622  	}
   623  
   624  	// Make sure the custom lambda that manages SNS notifications is provisioned.
   625  	configuratorResName, err := EnsureCustomResourceHandler(serviceName,
   626  		cfCustomResources.SESLambdaEventSource,
   627  		sourceArnExpression,
   628  		dependsOn,
   629  		template,
   630  		S3Bucket,
   631  		S3Key,
   632  		logger)
   633  
   634  	if nil != err {
   635  		return "", errors.Wrap(err, "Ensuring custom resource handler for SES")
   636  	}
   637  
   638  	// Add a custom resource invocation for this configuration
   639  	//////////////////////////////////////////////////////////////////////////////
   640  	newResource, newResourceError := newCloudFormationResource(cfCustomResources.SESLambdaEventSource, logger)
   641  	if nil != newResourceError {
   642  		return "", newResourceError
   643  	}
   644  	customResource := newResource.(*cfCustomResources.SESLambdaEventSourceResource)
   645  	customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn")
   646  	// The shared ruleset name used by all Sparta applications
   647  	customResource.RuleSetName = gocf.String("RuleSet")
   648  
   649  	///////////////////
   650  	// Build up the Rules
   651  	// If there aren't any rules, make one that forwards everything...
   652  	sesLength := 0
   653  	if perm.ReceiptRules == nil {
   654  		sesLength = 1
   655  	} else {
   656  		sesLength = len(perm.ReceiptRules)
   657  	}
   658  	sesRules := make([]*cfCustomResources.SESLambdaEventSourceResourceRule, sesLength)
   659  	if nil == perm.ReceiptRules {
   660  		sesRules[0] = &cfCustomResources.SESLambdaEventSourceResourceRule{
   661  			Name:        gocf.String("Default"),
   662  			Actions:     make([]*cfCustomResources.SESLambdaEventSourceResourceAction, 0),
   663  			ScanEnabled: gocf.Bool(false),
   664  			Enabled:     gocf.Bool(true),
   665  			Recipients:  []*gocf.StringExpr{},
   666  			TLSPolicy:   gocf.String("Optional"),
   667  		}
   668  	} else {
   669  		// Append all the user defined ones
   670  		for eachIndex, eachReceiptRule := range perm.ReceiptRules {
   671  			sesRules[eachIndex] = eachReceiptRule.toResourceRule(
   672  				serviceName,
   673  				gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"),
   674  				perm.MessageBodyStorage)
   675  		}
   676  	}
   677  
   678  	customResource.Rules = sesRules
   679  	// Name?
   680  	resourceInvokerName := CloudFormationResourceName("ConfigSNS",
   681  		lambdaLogicalCFResourceName,
   682  		perm.BasePermission.SourceAccount)
   683  
   684  	// Add it
   685  	cfResource := template.AddResource(resourceInvokerName, customResource)
   686  	cfResource.DependsOn = append(cfResource.DependsOn,
   687  		targetLambdaResourceName,
   688  		configuratorResName)
   689  	return "", nil
   690  }
   691  
   692  func (perm SESPermission) descriptionInfo() ([]descriptionNode, error) {
   693  	nodes := []descriptionNode{
   694  		{
   695  			Name:     "SimpleEmailService",
   696  			Relation: "All verified domain(s) email",
   697  		},
   698  	}
   699  	return nodes, nil
   700  }
   701  
   702  //
   703  // END - SESPermission
   704  ////////////////////////////////////////////////////////////////////////////////
   705  
   706  ////////////////////////////////////////////////////////////////////////////////
   707  // START - CloudWatchEventsRuleTarget
   708  //
   709  
   710  // CloudWatchEventsRuleTarget specifies additional input and JSON selection
   711  // paths to apply prior to forwarding the event to a lambda function
   712  type CloudWatchEventsRuleTarget struct {
   713  	Input     string
   714  	InputPath string
   715  }
   716  
   717  //
   718  // END - CloudWatchEventsRuleTarget
   719  ////////////////////////////////////////////////////////////////////////////////
   720  
   721  ////////////////////////////////////////////////////////////////////////////////
   722  // START - CloudWatchEventsRule
   723  //
   724  
   725  // CloudWatchEventsRule defines parameters for invoking a lambda function
   726  // in response to specific CloudWatchEvents or cron triggers
   727  type CloudWatchEventsRule struct {
   728  	Description string
   729  	// ArbitraryJSONObject filter for events as documented at
   730  	// http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CloudWatchEventsandEventPatterns.html
   731  	// Rules matches should use the JSON representation (NOT the string form).  Sparta will serialize
   732  	// the map[string]interface{} to a string form during CloudFormation Template
   733  	// marshalling.
   734  	EventPattern map[string]interface{} `json:"EventPattern,omitempty"`
   735  	// Schedule pattern per http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/ScheduledEvents.html
   736  	ScheduleExpression string
   737  	RuleTarget         *CloudWatchEventsRuleTarget `json:"RuleTarget,omitempty"`
   738  }
   739  
   740  // MarshalJSON customizes the JSON representation used when serializing to the
   741  // CloudFormation template representation.
   742  func (rule CloudWatchEventsRule) MarshalJSON() ([]byte, error) {
   743  	ruleJSON := map[string]interface{}{}
   744  
   745  	if rule.Description != "" {
   746  		ruleJSON["Description"] = rule.Description
   747  	}
   748  	if nil != rule.EventPattern {
   749  		eventPatternString, err := json.Marshal(rule.EventPattern)
   750  		if nil != err {
   751  			return nil, err
   752  		}
   753  		ruleJSON["EventPattern"] = string(eventPatternString)
   754  	}
   755  	if rule.ScheduleExpression != "" {
   756  		ruleJSON["ScheduleExpression"] = rule.ScheduleExpression
   757  	}
   758  	if nil != rule.RuleTarget {
   759  		ruleJSON["RuleTarget"] = rule.RuleTarget
   760  	}
   761  	return json.Marshal(ruleJSON)
   762  }
   763  
   764  //
   765  // END - CloudWatchEventsRule
   766  ////////////////////////////////////////////////////////////////////////////////
   767  
   768  ////////////////////////////////////////////////////////////////////////////////
   769  // START - CloudWatchEventsPermission
   770  //
   771  var cloudformationEventsSourceArnParts = []gocf.Stringable{}
   772  
   773  // CloudWatchEventsPermission struct implies that the CloudWatchEvent sources
   774  // should be configured as part of provisioning.  The BasePermission.SourceArn
   775  // isn't considered for this configuration. Each CloudWatchEventsRule struct
   776  // in the Rules map is used to register for push based event notifications via
   777  // `putRule` and `deleteRule`.
   778  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
   779  // for more information.
   780  type CloudWatchEventsPermission struct {
   781  	BasePermission
   782  	// Map of rule names to events that trigger the lambda function
   783  	Rules map[string]CloudWatchEventsRule
   784  }
   785  
   786  func (perm CloudWatchEventsPermission) export(serviceName string,
   787  	lambdaFunctionDisplayName string,
   788  	lambdaLogicalCFResourceName string,
   789  	template *gocf.Template,
   790  	S3Bucket string,
   791  	S3Key string,
   792  	logger *logrus.Logger) (string, error) {
   793  
   794  	// There needs to be at least one rule to apply
   795  	if len(perm.Rules) <= 0 {
   796  		return "", fmt.Errorf("function %s CloudWatchEventsPermission does not specify any expressions", lambdaFunctionDisplayName)
   797  	}
   798  
   799  	// Tell the user we're ignoring any Arns provided, since it doesn't make sense for this.
   800  	if nil != perm.BasePermission.SourceArn &&
   801  		perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...).String() != wildcardArn.String() {
   802  		logger.WithFields(logrus.Fields{
   803  			"Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...),
   804  		}).Warn("CloudWatchEvents do not support literal ARN values")
   805  	}
   806  
   807  	arnPermissionForRuleName := func(ruleName string) *gocf.StringExpr {
   808  		return gocf.Join("",
   809  			gocf.String("arn:aws:events:"),
   810  			gocf.Ref("AWS::Region"),
   811  			gocf.String(":"),
   812  			gocf.Ref("AWS::AccountId"),
   813  			gocf.String(":rule/"),
   814  			gocf.String(ruleName))
   815  	}
   816  
   817  	// Add the permission to invoke the lambda function
   818  	uniqueRuleNameMap := make(map[string]int)
   819  	for eachRuleName, eachRuleDefinition := range perm.Rules {
   820  
   821  		// We need a stable unique name s.t. the permission is properly configured...
   822  		uniqueRuleName := CloudFormationResourceName(eachRuleName, lambdaFunctionDisplayName, serviceName)
   823  		uniqueRuleNameMap[uniqueRuleName]++
   824  
   825  		// Add the permission
   826  		basePerm := BasePermission{
   827  			SourceArn: arnPermissionForRuleName(uniqueRuleName),
   828  		}
   829  		_, exportErr := basePerm.export(gocf.String(CloudWatchEventsPrincipal),
   830  			cloudformationEventsSourceArnParts,
   831  			lambdaFunctionDisplayName,
   832  			lambdaLogicalCFResourceName,
   833  			template,
   834  			S3Bucket,
   835  			S3Key,
   836  			logger)
   837  
   838  		if nil != exportErr {
   839  			return "", exportErr
   840  		}
   841  
   842  		cwEventsRuleTargetList := gocf.EventsRuleTargetList{}
   843  		cwEventsRuleTargetList = append(cwEventsRuleTargetList,
   844  			gocf.EventsRuleTarget{
   845  				Arn: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"),
   846  				ID:  gocf.String(uniqueRuleName),
   847  			},
   848  		)
   849  
   850  		// Add the rule
   851  		eventsRule := &gocf.EventsRule{
   852  			Name:        gocf.String(uniqueRuleName),
   853  			Description: gocf.String(eachRuleDefinition.Description),
   854  			Targets:     &cwEventsRuleTargetList,
   855  		}
   856  		if nil != eachRuleDefinition.EventPattern && eachRuleDefinition.ScheduleExpression != "" {
   857  			return "", fmt.Errorf("rule %s CloudWatchEvents specifies both EventPattern and ScheduleExpression", eachRuleName)
   858  		}
   859  		if nil != eachRuleDefinition.EventPattern {
   860  			eventsRule.EventPattern = eachRuleDefinition.EventPattern
   861  		} else if eachRuleDefinition.ScheduleExpression != "" {
   862  			eventsRule.ScheduleExpression = gocf.String(eachRuleDefinition.ScheduleExpression)
   863  		}
   864  		cloudWatchLogsEventResName := CloudFormationResourceName(fmt.Sprintf("%s-CloudWatchEventsRule", eachRuleName),
   865  			lambdaLogicalCFResourceName,
   866  			lambdaFunctionDisplayName)
   867  		template.AddResource(cloudWatchLogsEventResName, eventsRule)
   868  	}
   869  	// Validate it
   870  	for _, eachCount := range uniqueRuleNameMap {
   871  		if eachCount != 1 {
   872  			return "", fmt.Errorf("integrity violation for CloudWatchEvent Rulenames: %#v", uniqueRuleNameMap)
   873  		}
   874  	}
   875  	return "", nil
   876  }
   877  
   878  func (perm CloudWatchEventsPermission) descriptionInfo() ([]descriptionNode, error) {
   879  	var ruleTriggers = " "
   880  	for eachName, eachRule := range perm.Rules {
   881  		filter := eachRule.ScheduleExpression
   882  		if filter == "" && eachRule.EventPattern != nil {
   883  			filter = fmt.Sprintf("%v", eachRule.EventPattern["source"])
   884  		}
   885  		ruleTriggers = fmt.Sprintf("%s-(%s)\n%s", eachName, filter, ruleTriggers)
   886  	}
   887  	nodes := []descriptionNode{
   888  		{
   889  			Name:     "CloudWatch Events",
   890  			Relation: ruleTriggers,
   891  		},
   892  	}
   893  	return nodes, nil
   894  }
   895  
   896  //
   897  // END - CloudWatchEventsPermission
   898  ////////////////////////////////////////////////////////////////////////////////
   899  
   900  ////////////////////////////////////////////////////////////////////////////////
   901  // START - EventBridgeRule
   902  //
   903  
   904  // EventBridgeRule defines parameters for invoking a lambda function
   905  // in response to specific EventBridge triggers
   906  type EventBridgeRule struct {
   907  	Description  string
   908  	EventBusName string
   909  	// ArbitraryJSONObject filter for events as documented at
   910  	// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html#cfn-events-rule-eventpattern
   911  	// Rules matches should use the JSON representation (NOT the string form).  Sparta will serialize
   912  	// the map[string]interface{} to a string form during CloudFormation Template
   913  	// marshalling.
   914  	EventPattern map[string]interface{} `json:"EventPattern,omitempty"`
   915  	// Schedule pattern per
   916  	// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-rule.html#cfn-events-rule-scheduleexpression
   917  	ScheduleExpression string
   918  }
   919  
   920  // MarshalJSON customizes the JSON representation used when serializing to the
   921  // CloudFormation template representation.
   922  func (rule EventBridgeRule) MarshalJSON() ([]byte, error) {
   923  	ruleJSON := map[string]interface{}{}
   924  
   925  	ruleJSON["Description"] = marshalString(rule.Description)
   926  	ruleJSON["EventBusName"] = marshalString(rule.EventBusName)
   927  	if rule.EventPattern != nil {
   928  		ruleJSON["EventPattern"] = marshalInterface(rule.EventPattern)
   929  	}
   930  	if rule.ScheduleExpression != "" {
   931  		ruleJSON["ScheduleExpression"] = marshalString(rule.ScheduleExpression)
   932  	}
   933  	return json.Marshal(ruleJSON)
   934  }
   935  
   936  //
   937  // END - EventBridgeRule
   938  ////////////////////////////////////////////////////////////////////////////////
   939  
   940  ////////////////////////////////////////////////////////////////////////////////
   941  // START - EventBridgePermission
   942  //
   943  
   944  // EventBridgePermission struct implies that the EventBridge sources
   945  // should be configured as part of provisioning.  The BasePermission.SourceArn
   946  // isn't considered for this configuration. Each EventBridge Rule or Schedule struct
   947  // in the Rules map is used to register for push based event notifications via
   948  // `putRule` and `deleteRule`.
   949  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
   950  // for more information.
   951  type EventBridgePermission struct {
   952  	BasePermission
   953  	// EventBridgeRule for this permission
   954  	Rule *EventBridgeRule
   955  }
   956  
   957  func (perm EventBridgePermission) export(serviceName string,
   958  	lambdaFunctionDisplayName string,
   959  	lambdaLogicalCFResourceName string,
   960  	template *gocf.Template,
   961  	S3Bucket string,
   962  	S3Key string,
   963  	logger *logrus.Logger) (string, error) {
   964  
   965  	// There needs to be at least one rule to apply
   966  	if perm.Rule == nil {
   967  		return "", fmt.Errorf("function %s EventBridgePermission does not specify any EventBridgeRule",
   968  			lambdaFunctionDisplayName)
   969  	}
   970  
   971  	// Name for the rule...
   972  	eventBridgeRuleResourceName := CloudFormationResourceName(fmt.Sprintf("EventBridge-%s", lambdaLogicalCFResourceName),
   973  		lambdaFunctionDisplayName)
   974  
   975  	// Tell the user we're ignoring any Arns provided, since it doesn't make sense for this.
   976  	if nil != perm.BasePermission.SourceArn &&
   977  		perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...).String() != wildcardArn.String() {
   978  		logger.WithFields(logrus.Fields{
   979  			"Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...),
   980  		}).Warn("EventBridge Events do not support literal ARN values")
   981  	}
   982  
   983  	// Add the permission
   984  	basePerm := BasePermission{
   985  		SourceArn: gocf.GetAtt(eventBridgeRuleResourceName, "Arn"),
   986  	}
   987  	_, exportErr := basePerm.export(gocf.String(EventBridgePrincipal),
   988  		cloudformationEventsSourceArnParts,
   989  		lambdaFunctionDisplayName,
   990  		lambdaLogicalCFResourceName,
   991  		template,
   992  		S3Bucket,
   993  		S3Key,
   994  		logger)
   995  
   996  	if nil != exportErr {
   997  		return "", exportErr
   998  	}
   999  
  1000  	eventBridgeRuleTargetList := gocf.EventsRuleTargetList{}
  1001  	eventBridgeRuleTargetList = append(eventBridgeRuleTargetList,
  1002  		gocf.EventsRuleTarget{
  1003  			Arn: gocf.GetAtt(lambdaLogicalCFResourceName, "Arn"),
  1004  			ID:  gocf.String(serviceName),
  1005  		},
  1006  	)
  1007  	if nil != perm.Rule.EventPattern &&
  1008  		perm.Rule.ScheduleExpression != "" {
  1009  		return "", fmt.Errorf("rule %s EventBridge specifies both EventPattern and ScheduleExpression",
  1010  			perm.Rule)
  1011  	}
  1012  
  1013  	// Add the rule
  1014  	eventsRule := &gocf.EventsRule{
  1015  		Targets: &eventBridgeRuleTargetList,
  1016  	}
  1017  	if perm.Rule.EventBusName != "" {
  1018  		eventsRule.EventBusName = marshalString(perm.Rule.EventBusName)
  1019  	}
  1020  	// Setup the description placeholder...we'll set it in a bit...
  1021  	ruleDescription := ""
  1022  	if perm.Rule.EventPattern != nil {
  1023  		eventsRule.EventPattern = marshalInterface(perm.Rule.EventPattern)
  1024  		ruleDescription = fmt.Sprintf("%s (Stack: %s) event pattern subscriber",
  1025  			lambdaFunctionDisplayName,
  1026  			serviceName)
  1027  	} else if perm.Rule.ScheduleExpression != "" {
  1028  		eventsRule.ScheduleExpression = marshalString(perm.Rule.ScheduleExpression)
  1029  		ruleDescription = fmt.Sprintf("%s (Stack: %s) scheduled subscriber",
  1030  			lambdaFunctionDisplayName,
  1031  			serviceName)
  1032  	}
  1033  	eventsRule.Description = marshalString(ruleDescription)
  1034  	template.AddResource(eventBridgeRuleResourceName, eventsRule)
  1035  	return "", nil
  1036  }
  1037  
  1038  func (perm EventBridgePermission) descriptionInfo() ([]descriptionNode, error) {
  1039  	var ruleTriggers = " "
  1040  
  1041  	filter := perm.Rule.ScheduleExpression
  1042  	if filter == "" && perm.Rule.EventPattern != nil {
  1043  		filter = fmt.Sprintf("%v", perm.Rule.EventPattern)
  1044  	}
  1045  	ruleTriggers = fmt.Sprintf("EventBridge-(%s)\n%s", filter, ruleTriggers)
  1046  
  1047  	nodes := []descriptionNode{
  1048  		{
  1049  			Name:     "EventBridge Event",
  1050  			Relation: ruleTriggers,
  1051  		},
  1052  	}
  1053  	return nodes, nil
  1054  }
  1055  
  1056  //
  1057  // END - CloudWatchEventsPermission
  1058  ////////////////////////////////////////////////////////////////////////////////
  1059  
  1060  ////////////////////////////////////////////////////////////////////////////////
  1061  // START - CloudWatchLogsPermission
  1062  //
  1063  
  1064  // CloudWatchLogsSubscriptionFilter represents the CloudWatch Log filter
  1065  // information
  1066  type CloudWatchLogsSubscriptionFilter struct {
  1067  	FilterPattern string
  1068  	LogGroupName  string
  1069  }
  1070  
  1071  var cloudformationLogsSourceArnParts = []gocf.Stringable{
  1072  	gocf.String("arn:aws:logs:"),
  1073  }
  1074  
  1075  // CloudWatchLogsPermission struct implies that the corresponding
  1076  // CloudWatchLogsSubscriptionFilter definitions should be configured during
  1077  // stack provisioning.  The BasePermission.SourceArn isn't considered for
  1078  // this configuration operation.  Configuration of the remote push source
  1079  // is done via `putSubscriptionFilter` and `deleteSubscriptionFilter`.
  1080  // See http://docs.aws.amazon.com/lambda/latest/dg/intro-core-components.html#intro-core-components-event-sources
  1081  // for more information.
  1082  type CloudWatchLogsPermission struct {
  1083  	BasePermission
  1084  	// Map of filter names to the CloudWatchLogsSubscriptionFilter settings
  1085  	Filters map[string]CloudWatchLogsSubscriptionFilter
  1086  }
  1087  
  1088  func (perm CloudWatchLogsPermission) export(serviceName string,
  1089  	lambdaFunctionDisplayName string,
  1090  	lambdaLogicalCFResourceName string,
  1091  	template *gocf.Template,
  1092  	S3Bucket string,
  1093  	S3Key string,
  1094  	logger *logrus.Logger) (string, error) {
  1095  
  1096  	// If there aren't any expressions to register with?
  1097  	if len(perm.Filters) <= 0 {
  1098  		return "", fmt.Errorf("function %s CloudWatchLogsPermission does not specify any filters", lambdaFunctionDisplayName)
  1099  	}
  1100  
  1101  	// The principal is region specific, so build that up...
  1102  	regionalPrincipal := gocf.Join(".",
  1103  		gocf.String("logs"),
  1104  		gocf.Ref("AWS::Region"),
  1105  		gocf.String("amazonaws.com"))
  1106  
  1107  	// Tell the user we're ignoring any Arns provided, since it doesn't make sense for
  1108  	// this.
  1109  	if nil != perm.BasePermission.SourceArn &&
  1110  		perm.BasePermission.sourceArnExpr(cloudformationLogsSourceArnParts...).String() != wildcardArn.String() {
  1111  		logger.WithFields(logrus.Fields{
  1112  			"Arn": perm.BasePermission.sourceArnExpr(cloudformationEventsSourceArnParts...),
  1113  		}).Warn("CloudWatchLogs do not support literal ARN values")
  1114  	}
  1115  
  1116  	// Make sure we grant InvokeFunction privileges to CloudWatchLogs
  1117  	lambdaInvokePermission, err := perm.BasePermission.export(regionalPrincipal,
  1118  		cloudformationLogsSourceArnParts,
  1119  		lambdaFunctionDisplayName,
  1120  		lambdaLogicalCFResourceName,
  1121  		template,
  1122  		S3Bucket,
  1123  		S3Key,
  1124  		logger)
  1125  	if nil != err {
  1126  		return "", errors.Wrap(err, "Exporting regional CloudWatch log permission")
  1127  	}
  1128  
  1129  	// Then we need to uniqueify the rule names s.t. we prevent
  1130  	// collisions with other stacks.
  1131  	configurationResourceNames := make(map[string]int)
  1132  	// Store the last name.  We'll do a uniqueness check when exiting the loop,
  1133  	// and if that passes, the last name will also be the unique one.
  1134  	var configurationResourceName string
  1135  	// Create the CustomResource entries
  1136  	globallyUniqueFilters := make(map[string]CloudWatchLogsSubscriptionFilter, len(perm.Filters))
  1137  	for eachFilterName, eachFilter := range perm.Filters {
  1138  		filterPrefix := fmt.Sprintf("%s_%s", serviceName, eachFilterName)
  1139  		uniqueFilterName := CloudFormationResourceName(filterPrefix, lambdaLogicalCFResourceName)
  1140  		globallyUniqueFilters[uniqueFilterName] = eachFilter
  1141  
  1142  		// The ARN we supply to IAM is built up using the user supplied groupname
  1143  		cloudWatchLogsArn := gocf.Join("",
  1144  			gocf.String("arn:aws:logs:"),
  1145  			gocf.Ref("AWS::Region"),
  1146  			gocf.String(":"),
  1147  			gocf.Ref("AWS::AccountId"),
  1148  			gocf.String(":log-group:"),
  1149  			gocf.String(eachFilter.LogGroupName),
  1150  			gocf.String(":log-stream:*"))
  1151  
  1152  		lastConfigurationResourceName, ensureCustomHandlerError := EnsureCustomResourceHandler(serviceName,
  1153  			cfCustomResources.CloudWatchLogsLambdaEventSource,
  1154  			cloudWatchLogsArn,
  1155  			[]string{},
  1156  			template,
  1157  			S3Bucket,
  1158  			S3Key,
  1159  			logger)
  1160  		if nil != ensureCustomHandlerError {
  1161  			return "", errors.Wrap(err, "Ensuring CloudWatch permissions handler")
  1162  		}
  1163  		configurationResourceNames[configurationResourceName] = 1
  1164  		configurationResourceName = lastConfigurationResourceName
  1165  	}
  1166  	if len(configurationResourceNames) > 1 {
  1167  		return "", fmt.Errorf("internal integrity check failed. Multiple configurators (%d) provisioned for CloudWatchLogs",
  1168  			len(configurationResourceNames))
  1169  	}
  1170  
  1171  	// Get the single configurator name from the
  1172  
  1173  	// Add the custom resource that uses this...
  1174  	//////////////////////////////////////////////////////////////////////////////
  1175  
  1176  	newResource, newResourceError := newCloudFormationResource(cfCustomResources.CloudWatchLogsLambdaEventSource, logger)
  1177  	if nil != newResourceError {
  1178  		return "", newResourceError
  1179  	}
  1180  	customResource := newResource.(*cfCustomResources.CloudWatchLogsLambdaEventSourceResource)
  1181  	customResource.ServiceToken = gocf.GetAtt(configurationResourceName, "Arn")
  1182  	customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn")
  1183  	// Build up the filters...
  1184  	customResource.Filters = make([]*cfCustomResources.CloudWatchLogsLambdaEventSourceFilter, 0)
  1185  	for eachName, eachFilter := range globallyUniqueFilters {
  1186  		customResource.Filters = append(customResource.Filters,
  1187  			&cfCustomResources.CloudWatchLogsLambdaEventSourceFilter{
  1188  				Name:         gocf.String(eachName),
  1189  				Pattern:      gocf.String(eachFilter.FilterPattern),
  1190  				LogGroupName: gocf.String(eachFilter.LogGroupName),
  1191  			})
  1192  
  1193  	}
  1194  
  1195  	resourceInvokerName := CloudFormationResourceName("ConfigCloudWatchLogs",
  1196  		lambdaLogicalCFResourceName,
  1197  		perm.BasePermission.SourceAccount)
  1198  	// Add it
  1199  	cfResource := template.AddResource(resourceInvokerName, customResource)
  1200  
  1201  	cfResource.DependsOn = append(cfResource.DependsOn,
  1202  		lambdaInvokePermission,
  1203  		lambdaLogicalCFResourceName,
  1204  		configurationResourceName)
  1205  	return "", nil
  1206  }
  1207  
  1208  func (perm CloudWatchLogsPermission) descriptionInfo() ([]descriptionNode, error) {
  1209  	nodes := make([]descriptionNode, len(perm.Filters))
  1210  	nodeIndex := 0
  1211  	for eachFilterName, eachFilterDef := range perm.Filters {
  1212  		nodes[nodeIndex] = descriptionNode{
  1213  			Name:     describeInfoValue(eachFilterDef.LogGroupName),
  1214  			Relation: fmt.Sprintf("%s (%s)", eachFilterName, eachFilterDef.FilterPattern),
  1215  		}
  1216  		nodeIndex++
  1217  	}
  1218  	return nodes, nil
  1219  }
  1220  
  1221  //
  1222  // END - CloudWatchLogsPermission
  1223  ///////////////////////////////////////////////////////////////////////////////////
  1224  
  1225  ////////////////////////////////////////////////////////////////////////////////
  1226  // START - CodeCommitPermission
  1227  //
  1228  // arn:aws:codecommit:us-west-2:123412341234:myRepo
  1229  var codeCommitSourceArnParts = []gocf.Stringable{
  1230  	gocf.String("arn:aws:codecommit:"),
  1231  	gocf.Ref("AWS::Region"),
  1232  	gocf.String(":"),
  1233  	gocf.Ref("AWS::AccountId"),
  1234  	gocf.String(":"),
  1235  }
  1236  
  1237  // CodeCommitPermission struct encapsulates the data necessary
  1238  // to trigger the owning LambdaFunction in response to
  1239  // CodeCommit events
  1240  type CodeCommitPermission struct {
  1241  	BasePermission
  1242  	// RepositoryName
  1243  	RepositoryName *gocf.StringExpr
  1244  	// Branches to register for
  1245  	Branches []string `json:"branches,omitempty"`
  1246  	// Events to subscribe to. Defaults to "all" if empty.
  1247  	Events []string `json:"events,omitempty"`
  1248  }
  1249  
  1250  func (perm CodeCommitPermission) export(serviceName string,
  1251  	lambdaFunctionDisplayName string,
  1252  	lambdaLogicalCFResourceName string,
  1253  	template *gocf.Template,
  1254  	S3Bucket string,
  1255  	S3Key string,
  1256  	logger *logrus.Logger) (string, error) {
  1257  
  1258  	principal := gocf.Join("",
  1259  		gocf.String("codecommit."),
  1260  		gocf.Ref("AWS::Region"),
  1261  		gocf.String(".amazonaws.com"))
  1262  
  1263  	sourceArnExpression := perm.BasePermission.sourceArnExpr(codeCommitSourceArnParts...)
  1264  
  1265  	targetLambdaResourceName, err := perm.BasePermission.export(principal,
  1266  		codeCommitSourceArnParts,
  1267  		lambdaFunctionDisplayName,
  1268  		lambdaLogicalCFResourceName,
  1269  		template,
  1270  		S3Bucket,
  1271  		S3Key,
  1272  		logger)
  1273  
  1274  	if nil != err {
  1275  		return "", errors.Wrap(err, "Failed to export CodeCommit permission")
  1276  	}
  1277  
  1278  	// Make sure that the handler that manages triggers is registered.
  1279  	configuratorResName, err := EnsureCustomResourceHandler(serviceName,
  1280  		cfCustomResources.CodeCommitLambdaEventSource,
  1281  		sourceArnExpression,
  1282  		[]string{},
  1283  		template,
  1284  		S3Bucket,
  1285  		S3Key,
  1286  		logger)
  1287  
  1288  	if nil != err {
  1289  		return "", errors.Wrap(err, "Exporing CodeCommit permission handler")
  1290  	}
  1291  
  1292  	// Add a custom resource invocation for this configuration
  1293  	//////////////////////////////////////////////////////////////////////////////
  1294  	newResource, newResourceError := newCloudFormationResource(cfCustomResources.CodeCommitLambdaEventSource,
  1295  		logger)
  1296  	if nil != newResourceError {
  1297  		return "", newResourceError
  1298  	}
  1299  	repoEvents := perm.Events
  1300  	if len(repoEvents) <= 0 {
  1301  		repoEvents = []string{"all"}
  1302  	}
  1303  	customResource := newResource.(*cfCustomResources.CodeCommitLambdaEventSourceResource)
  1304  	customResource.ServiceToken = gocf.GetAtt(configuratorResName, "Arn")
  1305  	customResource.LambdaTargetArn = gocf.GetAtt(lambdaLogicalCFResourceName, "Arn")
  1306  	customResource.TriggerName = gocf.Ref(lambdaLogicalCFResourceName).String()
  1307  	customResource.RepositoryName = perm.RepositoryName
  1308  	customResource.Events = repoEvents
  1309  	customResource.Branches = perm.Branches
  1310  
  1311  	// Name?
  1312  	resourceInvokerName := CloudFormationResourceName("ConfigCodeCommit",
  1313  		lambdaLogicalCFResourceName,
  1314  		perm.BasePermission.SourceAccount)
  1315  
  1316  	// Add it
  1317  	cfResource := template.AddResource(resourceInvokerName, customResource)
  1318  	cfResource.DependsOn = append(cfResource.DependsOn,
  1319  		targetLambdaResourceName,
  1320  		configuratorResName)
  1321  	return "", nil
  1322  }
  1323  
  1324  func (perm CodeCommitPermission) descriptionInfo() ([]descriptionNode, error) {
  1325  	nodes := make([]descriptionNode, 0)
  1326  	if len(perm.Branches) <= 0 {
  1327  		nodes = append(nodes, descriptionNode{
  1328  			Name:     describeInfoValue(perm.SourceArn),
  1329  			Relation: "all",
  1330  		})
  1331  	} else {
  1332  		for _, eachBranch := range perm.Branches {
  1333  			filterRel := fmt.Sprintf("%s (%#v)",
  1334  				eachBranch,
  1335  				perm.Events)
  1336  			nodes = append(nodes, descriptionNode{
  1337  				Name:     describeInfoValue(perm.SourceArn),
  1338  				Relation: filterRel,
  1339  			})
  1340  		}
  1341  	}
  1342  	return nodes, nil
  1343  }
  1344  
  1345  // END - CodeCommitPermission
  1346  ///////////////////////////////////////////////////////////////////////////////////