diff --git a/engines/terraform/platform.go b/engines/terraform/platform.go index ab2de2d..1a301c1 100644 --- a/engines/terraform/platform.go +++ b/engines/terraform/platform.go @@ -51,7 +51,7 @@ type PlatformSpec struct { TopicBlueprints map[string]*ResourceBlueprint `json:"topics,omitempty" yaml:"topics,omitempty"` DatabaseBlueprints map[string]*ResourceBlueprint `json:"databases,omitempty" yaml:"databases,omitempty"` EntrypointBlueprints map[string]*ResourceBlueprint `json:"entrypoints" yaml:"entrypoints"` - InfraSpecs map[string]*ResourceBlueprint `json:"infra" yaml:"infra"` + InfraSpecs map[string]*ResourceBlueprint `json:"infra" yaml:"infra"` libraryReplacements map[libraryID]libraryVersion } @@ -371,6 +371,7 @@ type ResourceBlueprint struct { Properties map[string]interface{} `json:"properties" yaml:"properties"` DependsOn []string `json:"depends_on" yaml:"depends_on,omitempty"` Variables map[string]Variable `json:"variables" yaml:"variables,omitempty"` + UsableBy []string `json:"usable_by,omitempty" yaml:"usable_by,omitempty"` } func (r *ResourceBlueprint) ResolvePlugin(platform *PlatformSpec) (*Plugin, error) { @@ -393,6 +394,22 @@ func (r *ResourceBlueprint) ResolvePlugin(platform *PlatformSpec) (*Plugin, erro return &Plugin{Library: *lib, Name: r.Source.Plugin}, nil } +func (r *ResourceBlueprint) ValidateServiceAccess(serviceSubtype, resourceName, resourceType string) error { + // All service types are allowed if useable_by is empty (backward compatible) + if len(r.UsableBy) == 0 { + return nil + } + + if !slices.Contains(r.UsableBy, serviceSubtype) { + return fmt.Errorf( + "%s '%s' cannot be accessed by service subtype '%s': this %s blueprint is only usable by service types: %v", + resourceType, resourceName, serviceSubtype, resourceType, r.UsableBy, + ) + } + + return nil +} + type IdentitiesBlueprint struct { Identities []ResourceBlueprint `json:"identities" yaml:"identities"` } diff --git a/engines/terraform/platform_test.go b/engines/terraform/platform_test.go new file mode 100644 index 0000000..665b0a2 --- /dev/null +++ b/engines/terraform/platform_test.go @@ -0,0 +1,98 @@ +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBlueprint_ValidateServiceAccess(t *testing.T) { + tests := []struct { + name string + usableBy []string + serviceSubtype string + resourceName string + resourceType string + expectError bool + errorContains string + }{ + { + name: "empty usable_by allows all service types", + usableBy: []string{}, + serviceSubtype: "lambda", + resourceName: "my_bucket", + resourceType: "bucket", + expectError: false, + }, + { + name: "nil usable_by allows all service types", + usableBy: nil, + serviceSubtype: "fargate", + resourceName: "my_database", + resourceType: "database", + expectError: false, + }, + { + name: "service subtype in allowed list succeeds", + usableBy: []string{"web", "worker", "api"}, + serviceSubtype: "web", + resourceName: "users_db", + resourceType: "database", + expectError: false, + }, + { + name: "service subtype not in allowed list fails", + usableBy: []string{"web", "worker"}, + serviceSubtype: "lambda", + resourceName: "users_db", + resourceType: "database", + expectError: true, + errorContains: "database 'users_db' cannot be accessed by service subtype 'lambda'", + }, + { + name: "single allowed service type succeeds", + usableBy: []string{"fargate"}, + serviceSubtype: "fargate", + resourceName: "assets", + resourceType: "bucket", + expectError: false, + }, + { + name: "single allowed service type with different subtype fails", + usableBy: []string{"fargate"}, + serviceSubtype: "lambda", + resourceName: "assets", + resourceType: "bucket", + expectError: true, + errorContains: "bucket 'assets' cannot be accessed by service subtype 'lambda'", + }, + { + name: "error message includes allowed types list", + usableBy: []string{"web", "worker", "cron"}, + serviceSubtype: "lambda", + resourceName: "cache_db", + resourceType: "database", + expectError: true, + errorContains: "only usable by service types: [web worker cron]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + blueprint := &ResourceBlueprint{ + UsableBy: tt.usableBy, + } + + err := blueprint.ValidateServiceAccess(tt.serviceSubtype, tt.resourceName, tt.resourceType) + + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/engines/terraform/resource_handler.go b/engines/terraform/resource_handler.go index 9b3d614..2e38b03 100644 --- a/engines/terraform/resource_handler.go +++ b/engines/terraform/resource_handler.go @@ -152,6 +152,15 @@ func (td *TerraformDeployment) processBucketResources(appSpec *app_spec_schema.A return nil, fmt.Errorf("could not give access to bucket %s: service %s not found", intentName, serviceName) } + // Validate that this service subtype is allowed to access the bucket + serviceIntent, ok := appSpec.ServiceIntents[serviceName] + if !ok { + return nil, fmt.Errorf("could not validate access to bucket %s: service %s not found in application spec", intentName, serviceName) + } + if err := spec.ValidateServiceAccess(serviceIntent.GetSubType(), intentName, "bucket"); err != nil { + return nil, err + } + servicesInput[serviceName] = map[string]interface{}{ "actions": jsii.Strings(expandedActions...), "identities": idMap, @@ -237,6 +246,15 @@ func (td *TerraformDeployment) processDatabaseResources(appSpec *app_spec_schema return nil, fmt.Errorf("could not give access to database %s: service %s not found", intentName, serviceName) } + // Validate that this service subtype is allowed to access the database + serviceIntent, ok := appSpec.ServiceIntents[serviceName] + if !ok { + return nil, fmt.Errorf("could not validate access to database %s: service %s not found in application spec", intentName, serviceName) + } + if err := spec.ValidateServiceAccess(serviceIntent.GetSubType(), intentName, "database"); err != nil { + return nil, err + } + servicesInput[serviceName] = map[string]interface{}{ "actions": jsii.Strings(expandedActions...), "identities": idMap,