Compare commits

..
Author SHA1 Message Date
Dominik Schmidt 4636a9c696 fix(search): index a resource with out-of-range coordinates without a geopoint
Corrupt EXIF (latitude beyond 90, longitude beyond 180) made OpenSearch reject the whole document on the geo_point sibling while bleve indexed it at the pole. Both engines now skip the sibling and keep the location object; the parity suite pins it.
2026-09-08 18:38:12 +02:00
10 changed files with 67 additions and 19 deletions

No files matched your search

@@ -4,7 +4,6 @@ import (
"errors"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/activitylog/pkg/config"
"github.com/opencloud-eu/opencloud/services/activitylog/pkg/config/defaults"
@@ -35,8 +34,5 @@ func ParseConfig(cfg *config.Config) error {
// Validate validates the config
func Validate(cfg *config.Config) error {
if cfg.Events.Disabled && cfg.HTTP.Disabled {
return shared.AllComponentsDisabledError(cfg.Service.Name)
}
return nil
}
+4
View File
@@ -63,6 +63,10 @@ Store specific notes:
- When using `nats-js-kv` it is recommended to set `OC_CACHE_STORE_NODES` to the same value as `OC_EVENTS_ENDPOINT`. That way the cache uses the same nats instance as the event bus.
- When using the `nats-js-kv` store, it is possible to set `OC_CACHE_DISABLE_PERSISTENCE` to instruct nats to not persist cache data on disc.
### Auto-Accept Shares
When setting the `SHARING_AUTO_ACCEPT_SHARES` to `true` (sharing service), all incoming shares will be accepted automatically. Users can overwrite this setting individually in their profile. The deprecated `FRONTEND_AUTO_ACCEPT_SHARES` is still supported for backwards compatibility.
## Passwords
### The Password Policy
+5 -1
View File
@@ -40,7 +40,11 @@ func ParseConfig(cfg *config.Config) error {
func Validate(cfg *config.Config) error {
if cfg.HTTP.Disabled && cfg.Events.DisabledConsumer {
return shared.AllComponentsDisabledError(cfg.Service.Name)
// might be debatable, but this situation should be treated as an error,
// as the process wouldn't be able to serve either API and would thus be
// completely useless -- in that case, just don't start this service
// in the first place (especially since it's optional)
return shared.AllComponentsDisabledError("graph")
}
if cfg.TokenManager.JWTSecret == "" {
+6 -3
View File
@@ -4,7 +4,6 @@ import (
"errors"
occfg "github.com/opencloud-eu/opencloud/pkg/config"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/policies/pkg/config"
"github.com/opencloud-eu/opencloud/services/policies/pkg/config/defaults"
@@ -34,8 +33,12 @@ func ParseConfig(cfg *config.Config) error {
}
func Validate(cfg *config.Config) error {
if cfg.Events.Disabled && cfg.GRPC.Disabled {
return shared.AllComponentsDisabledError(cfg.Service.Name)
if cfg.GRPC.Disabled && cfg.Events.Disabled {
// might be debatable, but this situation should be treated as an error,
// as the process wouldn't be able to serve either API and would thus be
// completely useless -- in that case, just don't start this service
// in the first place (especially since it's optional)
return errors.New("both gRPC and events APIs are disabled by configuration; at least one must be enabled")
}
return nil
}
@@ -34,10 +34,6 @@ func ParseConfig(cfg *config.Config) error {
}
func Validate(cfg *config.Config) error {
if cfg.Events.Disabled && cfg.GRPC.Disabled {
return shared.AllComponentsDisabledError(cfg.Service.Name)
}
if cfg.TokenManager.JWTSecret == "" {
return shared.MissingJWTTokenError(cfg.Service.Name)
}
+5
View File
@@ -47,5 +47,10 @@ func addGeopointSibling(m map[string]any, dottedPath string) {
if !hasLon || !hasLat {
return
}
// corrupt EXIF: OpenSearch rejects the whole document on an out-of-range
// geo_point, bleve would index it at the pole; both index it without one
if lat < -90 || lat > 90 || lon < -180 || lon > 180 {
return
}
parent[leaf+GeopointSuffix] = map[string]any{"lat": lat, "lon": lon}
}
+21
View File
@@ -64,6 +64,27 @@ var _ = Describe("PrepareForIndex geopoint", func() {
Expect(gp["lon"]).To(Equal(lon))
})
It("skips out-of-range geopoints but keeps the object", func() {
type geoDoc struct {
Location *struct {
Longitude *float64 `json:"longitude,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
} `json:"location,omitempty"`
}
for _, c := range [][2]float64{{11.1, 100}, {11.1, -90.5}, {180.5, 49.4}, {-181, 49.4}} {
lon, lat := c[0], c[1]
doc := geoDoc{Location: &struct {
Longitude *float64 `json:"longitude,omitempty"`
Latitude *float64 `json:"latitude,omitempty"`
}{Longitude: &lon, Latitude: &lat}}
m, err := PrepareForIndex(doc, map[string]FieldOpts{"location": {Type: TypeGeopoint}})
Expect(err).ToNot(HaveOccurred())
Expect(m).To(HaveKey("location"), "lon=%v lat=%v", lon, lat)
Expect(m).ToNot(HaveKey("location"+GeopointSuffix), "lon=%v lat=%v", lon, lat)
}
})
It("skips incomplete geopoints", func() {
type geoDoc struct {
Location *struct {
+2
View File
@@ -687,9 +687,11 @@ Fixtures:
- `some_song.mp3`, ID = 1$1!5, MimeType = audio/mpeg
- `team.jpg`, ID = 1$1!6, MimeType = image/jpeg
- `lost.jpg`, ID = 1$1!7, MimeType = image/jpeg
| Case | Query | expected | bleve | OpenSearch | same? |
|---|---|---|---|---|---|
| METADATA-01 | `*song*` reads `Audio` | all 16 fields unchanged | all 16 fields unchanged | all 16 fields unchanged | ✅ |
| METADATA-02 | `*team*` reads `Location` | all 3 fields unchanged | all 3 fields unchanged | all 3 fields unchanged | ✅ |
| METADATA-04 | `*lost*` reads `Location` | Latitude=100, Longitude=11.1 | Latitude=100, Longitude=11.1 | Latitude=100, Longitude=11.1 | ✅ |
| METADATA-03 | `*team*` reads `Audio` | none | none | none | ✅ |
@@ -43,6 +43,17 @@ func metadataGroup() responseGroup {
}),
)
// corrupt EXIF: latitude beyond 90 must not lose the file from the index
lost := fixtureDoc("lost.jpg",
withID("1$1!7"),
withMime("image/jpeg"),
withLocation(&libregraph.GeoCoordinates{
Altitude: libregraph.PtrFloat64(0),
Latitude: libregraph.PtrFloat64(100),
Longitude: libregraph.PtrFloat64(11.1),
}),
)
indexed := []string{
"Album=Some Album",
"AlbumArtist=Some AlbumArtist",
@@ -70,7 +81,7 @@ func metadataGroup() responseGroup {
return responseGroup{
name: "metadata",
fixtures: []search.Resource{song, team},
fixtures: []search.Resource{song, team, lost},
cases: []responseCase{
{
id: 1, query: `*song*`, reads: "Audio", want: unchanged(indexed),
@@ -109,6 +120,17 @@ func metadataGroup() responseGroup {
})
}),
},
{
id: 4, query: `*lost*`, reads: "Location", want: []string{"Latitude=100", "Longitude=11.1"},
read: readsMany(func(m *searchMessage.Match) []string {
location := m.GetEntity().GetLocation()
return []string{
fmt.Sprintf("Latitude=%v", location.GetLatitude()),
fmt.Sprintf("Longitude=%v", location.GetLongitude()),
}
}),
},
{
id: 3, query: `*team*`, reads: "Audio", want: []string{"none"},
read: reads(func(m *searchMessage.Match) string {
+1 -6
View File
@@ -35,12 +35,7 @@ Share behavior can be configured via environment variables:
- Auto-acceptance of shares
- Share permissions and restrictions
### Auto-Accept Shares
When setting the `SHARING_AUTO_ACCEPT_SHARES` to `true` (sharing service), all
incoming shares will be accepted automatically. Users can overwrite this
setting individually in their profile. The deprecated
`FRONTEND_AUTO_ACCEPT_SHARES` is still supported for backwards compatibility.
See the `frontend` service README for more details on share-related configuration options.
## Scalability