Repository: weaviate/weaviate (go)

Issue:
Incorrect cref body can lead to panic
Via @laura-ham:

```python
import weaviate
import json
​
client = weaviate.Client("http://localhost:8083")
​
schema = {
    "actions": {
        "classes": [],
        "type": "action"
    },
    "things": {
        "classes": [
            {
                "class": "Paper",
                "vectorizeClassName": True,
                "description": "A publication paper",
                "properties": [
                    {
                        "dataType": [
                            "Journal"
                        ],
                        "description": "Information about the journal the paper was published in",
                        "name": "inJournal",
                        "vectorizePropertyName": True,
                        "index": True
                    }
                ]
            }, {
                "class": "Journal",
                "vectorizeClassName": True,
                "description": "A journal that publishes papers",
                "properties": [
                    {
                        "dataType": [
                            "string"
                        ],
                        "description": "Name of the journal",
                        "name": "name",
                        "vectorizePropertyName": True,
                        "index": True
                    }
                ]
            }
        ],
        "type": "thing"
    }
}
​
def import_paper_return_error():
    batch = weaviate.ThingsBatchRequest()
    paper_object = {}
    journal_name = 'testJournal'
    beacon_url = "weaviate://localhost/things/" + journal_name
    beacon = {"beacon": beacon_url}
    paper_object['inJournal'] = [{"beacon": beacon}]
    
    batch.add_thing(paper_object, "Paper") 
    
    try:
        result = client.batch.create_things(batch)
        return 'Success'
    except Exception as e:
        return 'ERROR: ', str(e)
​
def import_schema(schema):
    client.schema.delete_all()
    client.schema.create(schema)
​
if __name__ == "__main__":
    # Import the full schema to Weaviate
    import_schema(schema)
​
    result = import_paper_return_error()
    print(result)
```

Alternatively
```sh
curl \
    -X POST \
    -H "Content-Type: application/json" \
    -d '{
      "class": "Paper",
      "id": "36ddd591-2dee-4e7e-a3cc-eb86d30a4303",
      "schema": {
          "inJournal": [{
            "beacon": {
                "beacon": "weaviate://localhost/things/8e555f0d-8590-48c2-a9a6-70772ed14c0a"
            }
          }]
      }
  }' \
    http://localhost:8083/v1/things
curl: (52) Empty reply from server
```

Pull request:
Fix cref panic on invalid payload
Resolves #1253 

### What's being changed:
Additional type check + tests

The server logged the following messages with the example setup from the issue. The problem is in `/objects/validation/properties_validation.go:105` which has some assumptions on the correct data type.

```log
weaviate-1       | {"error":"interface conversion: interface {} is map[string]interface {}, not string","level":"error","method":"POST","msg":"interface conversion: interface {} is map[string]interface {}, not string","path":{"Scheme":"","Opaque":"","User":null,"Host":"","Path":"/v1/objects","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"time":"2024-01-30T10:03:06Z"}
weaviate-1       | {"action":"requests_total","api":"rest","class_name":"","error":"interface conversion: interface {} is map[string]interface {}, not string","level":"error","msg":"unexpected error","query_type":"","time":"2024-01-30T10:03:06Z"}
weaviate-1       | goroutine 11538 [running]:
weaviate-1       | runtime/debug.Stack()
weaviate-1       | 	/usr/local/go/src/runtime/debug/stack.go:24 +0x64
weaviate-1       | runtime/debug.PrintStack()
weaviate-1       | 	/usr/local/go/src/runtime/debug/stack.go:16 +0x1c
weaviate-1       | github.com/weaviate/weaviate/adapters/handlers/rest.handlePanics({0x1832028, 0x4003780380}, {0x181b5c0, 0x4003886378}, 0x40038b1200)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/adapters/handlers/rest/panics_middleware.go:80 +0x250
weaviate-1       | panic({0x117b2e0?, 0x4003366c00?})
weaviate-1       | 	/usr/local/go/src/runtime/panic.go:914 +0x218
weaviate-1       | github.com/weaviate/weaviate/usecases/objects/validation.(*Validator).properties(0x18?, {0x181bf98, 0x40033668d0}, 0x0?, 0x40038ae280, 0x7c6b14?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/usecases/objects/validation/properties_validation.go:105 +0x86c
weaviate-1       | github.com/weaviate/weaviate/usecases/objects/validation.(*Validator).Object(0x4002891c20?, {0x181bf98?, 0x40033668d0?}, 0x4002870c60?, 0x40038ae280?, 0x0?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/usecases/objects/validation/model_validation.go:88 +0xa4
weaviate-1       | github.com/weaviate/weaviate/usecases/objects.(*Manager).validateObjectAndNormalizeNames(0x4002891c20, {0x181bf98, 0x40033668d0}, 0x4003893190?, 0x0, 0x40038a8401?, 0x400243a9b8?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/usecases/objects/add.go:147 +0xd8
weaviate-1       | github.com/weaviate/weaviate/usecases/objects.(*Manager).addObjectToConnectorAndSchema(0x4002891c20, {0x181bf98, 0x40033668d0}, 0x400243aa40?, 0x40038ae280, 0xb25488?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/usecases/objects/add.go:109 +0x14c
weaviate-1       | github.com/weaviate/weaviate/usecases/objects.(*Manager).AddObject(0x4002891c20, {0x181bf98, 0x40033668d0}, 0x0?, 0x0?, 0x0?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/usecases/objects/add.go:58 +0x18c
weaviate-1       | github.com/weaviate/weaviate/adapters/handlers/rest.(*objectHandlers).addObject(0x4002967500, {0x40038b1400?, 0x40038ae280?, 0x0?}, 0x4002eced20?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/adapters/handlers/rest/handlers_objects.go:89 +0xc0
weaviate-1       | github.com/weaviate/weaviate/adapters/handlers/rest/operations/objects.ObjectsCreateHandlerFunc.Handle(0x4002a38d18?, {0x40038b1400?, 0x40038ae280?, 0x0?}, 0x4003118450?)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/adapters/handlers/rest/operations/objects/objects_create.go:32 +0x40
weaviate-1       | github.com/weaviate/weaviate/adapters/handlers/rest/operations/objects.(*ObjectsCreate).ServeHTTP(0x4002885470, {0x1816ae0, 0x40038baa80}, 0x40038b1400)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/adapters/handlers/rest/operations/objects/objects_create.go:81 +0x238
weaviate-1       | github.com/go-openapi/runtime/middleware.(*Context).RoutesHandler.NewOperationExecutor.func1({0x1816ae0, 0x40038baa80}, 0x40038b1400)
weaviate-1       | 	/go/pkg/mod/github.com/go-openapi/runtime@v0.24.2/middleware/operation.go:28 +0x5c
weaviate-1       | net/http.HandlerFunc.ServeHTTP(0x4003404bd0?, {0x1816ae0?, 0x40038baa80?}, 0x2377cf8?)
weaviate-1       | 	/usr/local/go/src/net/http/server.go:2136 +0x38
weaviate-1       | github.com/weaviate/weaviate/adapters/handlers/rest.configureAPI.makeSetupMiddlewares.func6.1({0x1816ae0, 0x40038baa80}, 0x40038b1400)
weaviate-1       | 	/go/src/github.com/weaviate/weaviate/adapters/handlers/rest/middlewares.go:43 +0xf4
weaviate-1       | net/http.HandlerFunc.ServeHTTP(0x4002a390f8?, {0x1816ae0?, 0x40038baa80?}, 0x117f3e0?)
weaviate-1       | 	/usr/local/go/src/net/http/server.go:2136 +0x38
weaviate-1       | github.com/go-openapi/runtime/middleware.NewRouter.func1({0x1816ae0, 0x40038baa80}, 0x40038b1200)
weaviate-1       | 	/go/pkg/mod/github.com/go-openapi/runtime@v0.24.2/middleware/router.go:78 +0x1c4
weaviate-1       | net/http.HandlerFunc.ServeHTTP(0x400313ef50?, {0x1816ae0?, 0x40038baa80?}, 0x10ac0a0?)
weaviate-1       | 	/usr/local/go/src/net/http/server.go:2136 +0x38
weaviate-1       | github.com/go-openapi/runtime/middleware.Redoc.func1({0x1816ae0, 0x40038baa80}, 0x4002a39168?)
weaviate-1       | 	/go/pkg/mod/github.com/go-openapi/runtime@v0.24.2/middleware/redoc.go:72 +0x1f4
weaviate-1       | net/http.HandlerFunc.ServeHTTP(0x1404595?, {0x1816ae0?, 0x40038baa80?}, 0x0?)
weaviate-1       | 	/usr/local/go/src/net/http/server.go:2136 +0x38
```
### Review checklist

- [ ] Documentation has been updated, if necessary. Link to changed documentation:
- [ ] Chaos pipeline run or not necessary. Link to pipeline:
- [ ] All new code is covered by tests where it is reasonable.
- [ ] Performance tests have been run or not necessary.


Patch:
diff --git a/usecases/objects/validation/properties_validation.go b/usecases/objects/validation/properties_validation.go
index d6bf2b21b7..e73295585c 100644
--- a/usecases/objects/validation/properties_validation.go
+++ b/usecases/objects/validation/properties_validation.go
@@ -100,9 +100,13 @@ func (v *Validator) properties(ctx context.Context, class *models.Class,
 			for i := range propertyValueSlice {
 				propertyValueMap, ok := propertyValueSlice[i].(map[string]interface{})
 				if !ok {
-					return fmt.Errorf("reference property is not a map %v", propertyValueMap)
+					return fmt.Errorf("reference property is not a map: %T", propertyValueMap)
 				}
-				beacon := propertyValueMap["beacon"].(string)
+				beacon, ok := propertyValueMap["beacon"].(string)
+				if !ok {
+					return fmt.Errorf("beacon property is not a string: %T", propertyValueMap["beacon"])
+				}
+
 				beaconParsed, err := crossref.Parse(beacon)
 				if err != nil {
 					return err
@@ -118,7 +122,7 @@ func (v *Validator) properties(ctx context.Context, class *models.Class,
 					}
 					toClass := prop.DataType[0] // datatype is the name of the class that is referenced
 					toBeacon := crossref.NewLocalhost(toClass, beaconParsed.TargetID).String()
-					propertyValue.([]interface{})[i].(map[string]interface{})["beacon"] = toBeacon
+					propertyValueMap["beacon"] = toBeacon
 				}
 			}
 		}


Test patch:
diff --git a/usecases/objects/validation/properties_validation_test.go b/usecases/objects/validation/properties_validation_test.go
index 10eaaf85d8..25e155ad9f 100644
--- a/usecases/objects/validation/properties_validation_test.go
+++ b/usecases/objects/validation/properties_validation_test.go
@@ -16,6 +16,11 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/go-openapi/strfmt"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/weaviate/weaviate/entities/additional"
+
 	"github.com/weaviate/weaviate/entities/models"
 	"github.com/weaviate/weaviate/entities/schema"
 	"github.com/weaviate/weaviate/usecases/config"
@@ -145,3 +150,70 @@ func TestValidator_extractAndValidateProperty(t *testing.T) {
 func getDataType(dataType schema.DataType) *schema.DataType {
 	return &dataType
 }
+
+func TestProperties(t *testing.T) {
+	const myBeacon = "weaviate://localhost/things/8e555f0d-8590-48c2-a9a6-70772ed14c0a"
+	myJournalClass := &models.Class{
+		Properties: []*models.Property{{Name: "inJournal", DataType: []string{"Journal"}}},
+	}
+	specs := map[string]struct {
+		class     *models.Class
+		obj       *models.Object
+		expErr    bool
+		expBeacon strfmt.URI
+	}{
+		"incorrect cref body - example from issue #1253": {
+			class: myJournalClass,
+			obj: &models.Object{
+				Properties: map[string]any{"inJournal": []any{map[string]any{
+					"beacon": map[string]any{"beacon": myBeacon},
+				}}},
+			},
+			expErr: true,
+		},
+		"complete beacon": {
+			class: myJournalClass,
+			obj: &models.Object{
+				Properties: map[string]any{"inJournal": []any{map[string]any{
+					"beacon": myBeacon,
+				}}},
+			},
+			expBeacon: myBeacon,
+		},
+		"beacon without class": {
+			class: myJournalClass,
+			obj: &models.Object{
+				Properties: map[string]any{"inJournal": []any{map[string]any{
+					"beacon": "weaviate://foo/8e555f0d-8590-48c2-a9a6-70772ed14c0a",
+				}}},
+			},
+			expBeacon: "weaviate://localhost/Journal/8e555f0d-8590-48c2-a9a6-70772ed14c0a",
+		},
+	}
+	for name, spec := range specs {
+		t.Run(name, func(t *testing.T) {
+			validator := &Validator{exists: func(_ context.Context, class string, _ strfmt.UUID, _ *additional.ReplicationProperties, _ string) (bool, error) {
+				return true, nil
+			}}
+			gotErr := validator.properties(context.Background(), spec.class, spec.obj, nil)
+			if spec.expErr {
+				require.Error(t, gotErr)
+				return
+			}
+			require.NoError(t, gotErr)
+			props := spec.obj.Properties
+			gotBeacon := extractBeacon(t, props)
+			assert.Equal(t, spec.expBeacon, gotBeacon)
+		})
+	}
+}
+
+func extractBeacon(t *testing.T, props models.PropertySchema) strfmt.URI {
+	require.IsType(t, map[string]any{}, props)
+	require.Contains(t, props.(map[string]any), "inJournal")
+	journalProp := props.(map[string]any)["inJournal"]
+	require.IsType(t, models.MultipleRef{}, journalProp)
+	require.Len(t, journalProp.(models.MultipleRef), 1)
+	gotBeacon := journalProp.(models.MultipleRef)[0].Beacon
+	return gotBeacon
+}