Improves validation and sanitization for CPA fields and values (#30694)

This change automatically removes options and sync attributes when
sanitizing fields that don't support them. As per values, it returns
an error when the value for a text type field is longer than the 64
characters limit we're currently applying.

The PR fixes a bug on the create CPA field endpoint that was causing
the attrs of the CPAField not to be decoded correctly.

Co-authored-by: Miguel de la Cruz <miguel@ctrlz.es>
Этот коммит содержится в:
Miguel de la Cruz
2025-04-16 16:04:30 +02:00
коммит произвёл GitHub
родитель 495a49b896
Коммит 3df7bfca88
3 изменённых файлов: 309 добавлений и 2 удалений

Просмотреть файл

@@ -46,11 +46,13 @@ const (
CustomProfileAttributesVisibilityWhenSet = "when_set"
CustomProfileAttributesVisibilityAlways = "always"
CustomProfileAttributesVisibilityDefault = CustomProfileAttributesVisibilityWhenSet
)
const (
// CPA options
CPAOptionNameMaxLength = 128
CPAOptionColorMaxLength = 128
// CPA value constraints
CPAValueTypeTextMaxLength = 64
)
func IsKnownCPAValueType(valueType string) bool {
@@ -146,7 +148,28 @@ func (c *CPAField) ToPropertyField() *PropertyField {
return &pf
}
// SupportsOptions checks the CPAField type and determines if the type
// supports the use of options
func (c *CPAField) SupportsOptions() bool {
return c.Type == PropertyFieldTypeSelect || c.Type == PropertyFieldTypeMultiselect
}
// SupportsSyncing checks the CPAField type and determines if it
// supports syncing with external sources of truth
func (c *CPAField) SupportsSyncing() bool {
return c.Type == PropertyFieldTypeText
}
func (c *CPAField) SanitizeAndValidate() *AppError {
// first we clean unused attributes depending on the field type
if !c.SupportsOptions() {
c.Attrs.Options = nil
}
if !c.SupportsSyncing() {
c.Attrs.LDAP = ""
c.Attrs.SAML = ""
}
switch c.Type {
case PropertyFieldTypeText:
if valueType := strings.TrimSpace(c.Attrs.ValueType); valueType != "" {
@@ -230,6 +253,10 @@ func SanitizeAndValidatePropertyValue(cpaField *CPAField, rawValue json.RawMessa
value = strings.TrimSpace(value)
if fieldType == PropertyFieldTypeText {
if len(value) > CPAValueTypeTextMaxLength {
return nil, fmt.Errorf("value too long")
}
if cpaField.Attrs.ValueType == CustomProfileAttributesValueTypeEmail && !IsValidEmail(value) {
return nil, fmt.Errorf("invalid email")
}

Просмотреть файл

@@ -481,6 +481,192 @@ func TestCPAField_SanitizeAndValidate(t *testing.T) {
expectError: true,
errorId: "app.custom_profile_attributes.sanitize_and_validate.app_error",
},
// Test options cleaning for types that don't support options
{
name: "text field with options should clean options",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeText,
},
Attrs: CPAAttrs{
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
Options: nil, // Options should be cleaned
},
},
{
name: "date field with options should clean options",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeDate,
},
Attrs: CPAAttrs{
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
Options: nil, // Options should be cleaned
},
},
{
name: "user field with options should clean options",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeUser,
},
Attrs: CPAAttrs{
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
Options: nil, // Options should be cleaned
},
},
// Test options preservation for types that support options
{
name: "select field with options should preserve options",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeSelect,
},
Attrs: CPAAttrs{
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
Options: PropertyOptions[*CustomProfileAttributesSelectOption]{
{Name: "Option 1", Color: "#123456"},
},
},
},
{
name: "multiselect field with options should preserve options",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeMultiselect,
},
Attrs: CPAAttrs{
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
Options: PropertyOptions[*CustomProfileAttributesSelectOption]{
{Name: "Option 1", Color: "#123456"},
},
},
},
// Test syncing attributes cleaning for types that don't support syncing
{
name: "select field with LDAP and SAML should clean syncing attributes",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeSelect,
},
Attrs: CPAAttrs{
LDAP: "ldap_attribute",
SAML: "saml_attribute",
Options: []*CustomProfileAttributesSelectOption{
{
ID: NewId(),
Name: "Option 1",
Color: "#123456",
},
},
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
LDAP: "", // Should be cleaned
SAML: "", // Should be cleaned
Options: PropertyOptions[*CustomProfileAttributesSelectOption]{
{Name: "Option 1", Color: "#123456"},
},
},
},
{
name: "date field with LDAP and SAML should clean syncing attributes",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeDate,
},
Attrs: CPAAttrs{
LDAP: "ldap_attribute",
SAML: "saml_attribute",
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
LDAP: "", // Should be cleaned
SAML: "", // Should be cleaned
},
},
// Test syncing attributes preservation for types that support syncing
{
name: "text field with LDAP and SAML should preserve syncing attributes",
field: &CPAField{
PropertyField: PropertyField{
Type: PropertyFieldTypeText,
},
Attrs: CPAAttrs{
LDAP: "ldap_attribute",
SAML: "saml_attribute",
},
},
expectError: false,
expectedAttrs: CPAAttrs{
Visibility: CustomProfileAttributesVisibilityDefault,
LDAP: "ldap_attribute", // Should be preserved
SAML: "saml_attribute", // Should be preserved
},
},
}
for _, tt := range tests {
@@ -539,6 +725,13 @@ func TestSanitizeAndValidatePropertyValue(t *testing.T) {
require.Error(t, err)
require.Contains(t, err.Error(), "json: cannot unmarshal number into Go value of type string")
})
t.Run("value too long", func(t *testing.T) {
longValue := strings.Repeat("a", CPAValueTypeTextMaxLength+1)
_, err := SanitizeAndValidatePropertyValue(&CPAField{PropertyField: PropertyField{Type: PropertyFieldTypeText}}, json.RawMessage(fmt.Sprintf(`"%s"`, longValue)))
require.Error(t, err)
require.Equal(t, "value too long", err.Error())
})
})
t.Run("date field type", func(t *testing.T) {