From b840138e09d6de10962f2ab05d1d45e40ed0ae5f Mon Sep 17 00:00:00 2001 From: Kilian Boute Date: Tue, 25 Aug 2026 14:01:59 +0000 Subject: [PATCH] fix(tenancy): require scoped cloud-key agreement --- pkg/tenancy/device.go | 16 ++++++++++++++-- pkg/tenancy/device_test.go | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/pkg/tenancy/device.go b/pkg/tenancy/device.go index d25c950..c4b78b0 100644 --- a/pkg/tenancy/device.go +++ b/pkg/tenancy/device.go @@ -89,6 +89,7 @@ type Ownership struct { type DeviceLookup struct { DeviceId primitive.ObjectID DeviceKey string + CloudKey string OrganisationId primitive.ObjectID ProjectId primitive.ObjectID LegacyOwnerId primitive.ObjectID @@ -213,10 +214,12 @@ func (r *DeviceResolver) LoadScopedDevice(ctx context.Context, lookup DeviceLook func scopedDeviceFilter(lookup DeviceLookup) (bson.M, error) { if !lookup.DeviceId.IsZero() { - return bson.M{ + filter := bson.M{ properties.DeviceId: lookup.DeviceId, properties.DeviceKey: lookup.DeviceKey, - }, nil + } + addCloudKeyFilter(filter, lookup.CloudKey) + return filter, nil } legacyOwnerId := lookup.LegacyOwnerId @@ -236,6 +239,7 @@ func scopedDeviceFilter(lookup DeviceLookup) (bson.M, error) { ) for _, arm := range filter["$or"].([]bson.M) { arm[properties.DeviceKey] = lookup.DeviceKey + addCloudKeyFilter(arm, lookup.CloudKey) } return filter, nil } @@ -248,6 +252,7 @@ func scopedDeviceFilter(lookup DeviceLookup) (bson.M, error) { ) for _, arm := range filter["$or"].([]bson.M) { arm[properties.DeviceKey] = lookup.DeviceKey + addCloudKeyFilter(arm, lookup.CloudKey) } return filter, nil } @@ -260,12 +265,19 @@ func scopedDeviceFilter(lookup DeviceLookup) (bson.M, error) { for field, test := range database.MissingCanonicalOrganisation() { filter[field] = test } + addCloudKeyFilter(filter, lookup.CloudKey) return filter, nil } return nil, fmt.Errorf("%w for key %q", ErrDeviceScopeRequired, lookup.DeviceKey) } +func addCloudKeyFilter(filter bson.M, cloudKey string) { + if cloudKey != "" { + filter["analytics.cloudpublickey"] = cloudKey + } +} + // ResolveDeviceByKey loads a device and resolves its ownership in one step. func (r *DeviceResolver) ResolveDeviceByKey(ctx context.Context, deviceKey string) (models.Device, Ownership, error) { device, err := r.LoadDevice(ctx, deviceKey) diff --git a/pkg/tenancy/device_test.go b/pkg/tenancy/device_test.go index ba91584..c374696 100644 --- a/pkg/tenancy/device_test.go +++ b/pkg/tenancy/device_test.go @@ -332,6 +332,7 @@ func TestLoadScopedDeviceAllowsSameKeyInDifferentProjects(t *testing.T) { device, err := resolver.LoadScopedDevice(context.Background(), DeviceLookup{ DeviceKey: "device-1", + CloudKey: "cloud-key-1", OrganisationId: organisationId, ProjectId: projectId, }) @@ -344,7 +345,7 @@ func TestLoadScopedDeviceAllowsSameKeyInDifferentProjects(t *testing.T) { filter := mock.FindCalls[0].Filter.(bson.M) for _, arm := range filter["$or"].([]bson.M) { - if arm[properties.DeviceKey] != "device-1" || arm[properties.DeviceProjectId] != projectId { + if arm[properties.DeviceKey] != "device-1" || arm[properties.DeviceProjectId] != projectId || arm["analytics.cloudpublickey"] != "cloud-key-1" { t.Fatalf("project-scoped arm = %#v", arm) } }