Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions cli/pkg/schema/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,77 @@ import (
"fmt"
"regexp"
"slices"
"strings"

"github.com/samber/lo"
"github.com/xeipuuv/gojsonschema"
)

func (a *Application) checkNoEnvVarCollisions() []gojsonschema.ResultError {
// Perform additional validation checks on the application
func (a *Application) IsValid() []gojsonschema.ResultError {
// Check the names of all resources are unique
violations := a.checkNoNameConflicts()
violations = append(violations, a.checkNoReservedNames()...)
violations = append(violations, a.checkSnakeCaseNames()...)
violations = append(violations, a.checkNoEnvVarCollisions()...)
violations = append(violations, a.checkAccessPermissions()...)

return violations
}

func (a *Application) checkAccessPermissions() []gojsonschema.ResultError {
violations := []gojsonschema.ResultError{}
envVarMap := map[string]string{}

// Validate bucket actions
validBucketActions := []string{"read", "write", "delete"}

for name, intent := range a.BucketIntents {
for serviceName, actions := range intent.Access {
invalidActions, ok := hasInvalidActions(actions, validBucketActions)
if !ok {
key := fmt.Sprintf("buckets.%s.access.%s", name, serviceName)
err := fmt.Sprintf("Invalid database %s: %s. Valid actions are: %s", pluralise("action", len(invalidActions)), strings.Join(invalidActions, ", "), strings.Join(validBucketActions, ", "))
violations = append(violations, newValidationError(key, err))
}
}
}

// Validate database actions
validDatabaseActions := []string{"query", "mutate"}

for name, intent := range a.DatabaseIntents {
if existingName, ok := envVarMap[intent.EnvVarKey]; ok {
violations = append(violations, newValidationError(fmt.Sprintf("databases.%s", name), fmt.Sprintf("env var %s is already in use by %s", intent.EnvVarKey, existingName)))
continue
for serviceName, actions := range intent.Access {
invalidActions, ok := hasInvalidActions(actions, validDatabaseActions)
if !ok {
key := fmt.Sprintf("databases.%s.access.%s", name, serviceName)
err := fmt.Sprintf("Invalid database %s: %s. Valid actions are: %s", pluralise("action", len(invalidActions)), strings.Join(invalidActions, ", "), strings.Join(validDatabaseActions, ", "))
violations = append(violations, newValidationError(key, err))
}
}
envVarMap[intent.EnvVarKey] = name
}

return violations
}

// Perform additional validation checks on the application
func (a *Application) IsValid() []gojsonschema.ResultError {
// Check the names of all resources are unique
violations := a.checkNoNameConflicts()
violations = append(violations, a.checkNoReservedNames()...)
violations = append(violations, a.checkSnakeCaseNames()...)
violations = append(violations, a.checkNoEnvVarCollisions()...)
func pluralise(word string, count int) string {
output := word
if count > 1 {
output += "s"
}
return output
}

return violations
func hasInvalidActions(actions []string, validActions []string) ([]string, bool) {
invalidActions := []string{}
validActions = append(validActions, "all")

for _, action := range actions {
if !lo.Contains(validActions, strings.ToLower(action)) {
invalidActions = append(invalidActions, action)
}
}

return invalidActions, len(invalidActions) == 0
}

func (a *Application) checkNoNameConflicts() []gojsonschema.ResultError {
Expand Down Expand Up @@ -157,3 +200,18 @@ func (a *Application) checkSnakeCaseNames() []gojsonschema.ResultError {

return violations
}

func (a *Application) checkNoEnvVarCollisions() []gojsonschema.ResultError {
violations := []gojsonschema.ResultError{}
envVarMap := map[string]string{}

for name, intent := range a.DatabaseIntents {
if existingName, ok := envVarMap[intent.EnvVarKey]; ok {
violations = append(violations, newValidationError(fmt.Sprintf("databases.%s", name), fmt.Sprintf("env var %s is already in use by %s", intent.EnvVarKey, existingName)))
continue
}
envVarMap[intent.EnvVarKey] = name
}

return violations
}
32 changes: 32 additions & 0 deletions engines/terraform/access.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package terraform

import "github.com/samber/lo"

type ResourceType string

const (
Database ResourceType = "database"
Bucket ResourceType = "bucket"
)

const AllAccess = "all"

var accessTypes = map[ResourceType][]string{
Database: {"query", "mutate"},
Bucket: {"read", "write", "delete"},
}

// ExpandActions expands 'all' in an actions array whilst maintaining deduplication
func ExpandActions(actions []string, resourceType ResourceType) []string {
var expanded []string

for _, action := range actions {
if action == AllAccess {
expanded = append(expanded, accessTypes[resourceType]...)
} else {
expanded = append(expanded, action)
}
}

return lo.Uniq(expanded)
}
8 changes: 6 additions & 2 deletions engines/terraform/resource_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@ func (td *TerraformDeployment) processBucketResources(appSpec *app_spec_schema.A
servicesInput := map[string]any{}
if access, ok := bucketIntent.GetAccess(); ok {
for serviceName, actions := range access {
expandedActions := ExpandActions(actions, Bucket)

idMap, ok := td.serviceIdentities[serviceName]
if !ok {
return nil, fmt.Errorf("could not give access to bucket %s: service %s not found", intentName, serviceName)
}

servicesInput[serviceName] = map[string]interface{}{
"actions": jsii.Strings(actions...),
"actions": jsii.Strings(expandedActions...),
"identities": idMap,
}
}
Expand Down Expand Up @@ -205,13 +207,15 @@ func (td *TerraformDeployment) processDatabaseResources(appSpec *app_spec_schema
servicesInput := map[string]any{}
if access, ok := databaseIntent.GetAccess(); ok {
for serviceName, actions := range access {
expandedActions := ExpandActions(actions, Database)

idMap, ok := td.serviceIdentities[serviceName]
if !ok {
return nil, fmt.Errorf("could not give access to database %s: service %s not found", intentName, serviceName)
}

servicesInput[serviceName] = map[string]interface{}{
"actions": jsii.Strings(actions...),
"actions": jsii.Strings(expandedActions...),
"identities": idMap,
}
}
Expand Down