Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 38 additions & 2 deletions pkg/cli/lambda_server__added.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"os"
"runtime/debug"
"strings"
"sync"
"time"

Expand All @@ -22,6 +23,7 @@ import (
"github.com/go-jose/go-jose/v4"
"github.com/maypok86/otter/v2"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
Expand Down Expand Up @@ -334,7 +336,7 @@ func OptionallyAddLambdaCommand[T field.Configurable](
}

connectorConfig := configStruct.AsMap()
effectiveConfig := effectiveLambdaConfig(v, connectorConfig)
effectiveConfig := effectiveLambdaConfig(v, connectorConfig, connectorSchema)
logLevelConfig, err := lambdaLogLevelConfigFromViper(effectiveConfig)
if err != nil {
return nil, err
Expand Down Expand Up @@ -488,14 +490,48 @@ func cloneViperSettings(v *viper.Viper) *viper.Viper {
return cloned
}

func effectiveLambdaConfig(v *viper.Viper, connectorConfig map[string]any) *viper.Viper {
func effectiveLambdaConfig(v *viper.Viper, connectorConfig map[string]any, connectorSchema field.Configuration) *viper.Viper {
effectiveConfig := cloneViperSettings(v)
stringMapFields := lambdaStringMapFields(connectorSchema)
for key, value := range connectorConfig {
if _, ok := stringMapFields[strings.ToLower(key)]; ok {
value = lambdaStringMapValueForViper(value)
}
effectiveConfig.Set(key, value)
}
return effectiveConfig
}

func lambdaStringMapFields(connectorSchema field.Configuration) map[string]struct{} {
out := make(map[string]struct{})
for _, schemaField := range connectorSchema.Fields {
if schemaField.Variant == field.StringMapVariant {
out[strings.ToLower(schemaField.FieldName)] = struct{}{}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to lower the field names here at all? WHat is consuming out that needs lowered names?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example config value:

map[string]any{
	"user-custom-fields": map[string]any{
		"customString10":     "Custom String 10",
		"departmentNav/name": "Department",
	},
}

Non-Lambda CLI

Top-level field names are effectively Viper-managed and case-insensitive. The nested map keys are the customer’s payload and should stay as-is.

v.GetStringMapString("user-custom-fields")
// map[string]string{
//   "customString10":     "Custom String 10",
//   "departmentNav/name": "Department",
// }

Lambda, Typed Connector

This is the SuccessFactors-style path. With PR #856, the typed generated config decodes the raw lambda payload directly, bypassing Viper.Set for the connector struct.

cfg.UserCustomFields
// map[string]any{
//   "customString10":     "Custom String 10",
//   "departmentNav/name": "Department",
// }

PR #857 does not materially affect this path.

Lambda, Axiomatic / *viper.Viper

This is the future footgun path. Axiomatic receives *viper.Viper, so without PR #857 the nested keys would pass through Viper.Set as a map and become:

map[string]any{
	"customstring10":     "Custom String 10",
	"departmentnav/name": "Department",
}

With PR #857, only schema-declared StringMapField values are converted before Viper.Set:

"user-custom-fields" => `{"customString10":"Custom String 10","departmentNav/name":"Department"}`

Then Axiomatic reads it normally:

v.GetStringMapString("user-custom-fields")
// map[string]string{
//   "customString10":     "Custom String 10",
//   "departmentNav/name": "Department",
// }

we haven't used this field type yet for axiomatic but i think we want to set the precident as wanting case sensative - map keys are often external identifiers, not user-facing option names. They may be JSON paths, OData paths, SCIM attributes, API field names, headers, claim names, or vendor-specific IDs. Many of those are case-sensitive, or at least case-preserving.

}
}
for _, fieldGroup := range connectorSchema.FieldGroups {
for _, schemaField := range fieldGroup.Fields {
if schemaField.Variant == field.StringMapVariant {
out[strings.ToLower(schemaField.FieldName)] = struct{}{}
}
}
}
return out
}

func lambdaStringMapValueForViper(value any) any {
if _, ok := value.(string); ok {
return value
}
// Viper.GetStringMap and GetStringMapString parse JSON strings, while
// Viper.Set lowercases nested map[string]any keys.
encoded, err := json.Marshal(cast.ToStringMapString(value))
if err != nil {
return value
}
return string(encoded)
}

// Typed configs must decode the raw lambda payload directly. Decoding them from
// effectiveConfig would send StringMap values through Viper.Set, which lowercases
// nested map keys.
Expand Down
46 changes: 44 additions & 2 deletions pkg/cli/lambda_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/field"
"github.com/spf13/viper"
)

Expand Down Expand Up @@ -107,7 +108,7 @@ func TestEffectiveLambdaConfigSyncResourceTypeIDs(t *testing.T) {
"sync-resource-types": []any{"user", "group"},
}

effectiveConfig := effectiveLambdaConfig(base, connectorConfig)
effectiveConfig := effectiveLambdaConfig(base, connectorConfig, field.Configuration{})

got := effectiveConfig.GetStringSlice("sync-resource-types")
want := []string{"user", "group"}
Expand All @@ -116,6 +117,44 @@ func TestEffectiveLambdaConfigSyncResourceTypeIDs(t *testing.T) {
}
}

func TestEffectiveLambdaConfigPreservesStringMapKeyCaseForViper(t *testing.T) {
t.Parallel()

base := viper.New()
connectorConfig := map[string]any{
"user-custom-fields": map[string]any{
"departmentNav/name": "Department",
"customString10": "Custom String 10",
"isActive": true,
},
}
schema := field.NewConfiguration([]field.SchemaField{
field.StringMapField("user-custom-fields"),
})

effectiveConfig := effectiveLambdaConfig(base, connectorConfig, schema)

gotStringMap := effectiveConfig.GetStringMap("user-custom-fields")
wantStringMap := map[string]any{
"departmentNav/name": "Department",
"customString10": "Custom String 10",
"isActive": "true",
}
if !reflect.DeepEqual(gotStringMap, wantStringMap) {
t.Fatalf("GetStringMap(user-custom-fields) = %#v, want %#v", gotStringMap, wantStringMap)
}

gotStringMapString := effectiveConfig.GetStringMapString("user-custom-fields")
wantStringMapString := map[string]string{
"departmentNav/name": "Department",
"customString10": "Custom String 10",
"isActive": "true",
}
if !reflect.DeepEqual(gotStringMapString, wantStringMapString) {
t.Fatalf("GetStringMapString(user-custom-fields) = %#v, want %#v", gotStringMapString, wantStringMapString)
}
}

func TestMakeLambdaConnectorConfigurationPreservesStringMapKeyCase(t *testing.T) {
t.Parallel()

Expand All @@ -127,7 +166,10 @@ func TestMakeLambdaConnectorConfigurationPreservesStringMapKeyCase(t *testing.T)
},
}

config, err := makeLambdaConnectorConfiguration[*testLambdaStringMapConfig](base, effectiveLambdaConfig(base, connectorConfig), connectorConfig)
schema := field.NewConfiguration([]field.SchemaField{
field.StringMapField("user-custom-fields"),
})
config, err := makeLambdaConnectorConfiguration[*testLambdaStringMapConfig](base, effectiveLambdaConfig(base, connectorConfig, schema), connectorConfig)
if err != nil {
t.Fatalf("makeLambdaConnectorConfiguration: %v", err)
}
Expand Down
Loading