From f74b6d0583c26218e1cddef04754c7e1694c6ba8 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Fri, 9 Jun 2023 12:04:46 +0200 Subject: [PATCH 1/8] dcs: Get device EUIs from the Entity Registry --- .../enddevices/enddevices.go | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/deviceclaimingserver/enddevices/enddevices.go b/pkg/deviceclaimingserver/enddevices/enddevices.go index f3ee6c7b52..45daff7e11 100644 --- a/pkg/deviceclaimingserver/enddevices/enddevices.go +++ b/pkg/deviceclaimingserver/enddevices/enddevices.go @@ -151,7 +151,7 @@ func WithDeviceRegistry(reg ttnpb.EndDeviceRegistryClient) Option { } var ( - errNoEUI = errors.DefineInvalidArgument("no_eui", "DevEUI/JoinEUI not found in request") + errNoEUI = errors.DefineInvalidArgument("no_eui", "DevEUI/JoinEUI not set for device") errClaimingNotSupported = errors.DefineAborted("claiming_not_supported", "claiming not supported for JoinEUI `{eui}`") ) @@ -177,22 +177,33 @@ func (upstream *Upstream) Claim( // Unclaim implements EndDeviceClaimingServer. func (upstream *Upstream) Unclaim(ctx context.Context, in *ttnpb.EndDeviceIdentifiers) (*emptypb.Empty, error) { - if in.DevEui == nil || in.JoinEui == nil { - return nil, errNoEUI.New() - } err := upstream.requireRights(ctx, in, &ttnpb.Rights{ Rights: []ttnpb.Right{ + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, }, }) if err != nil { return nil, err } - claimer := upstream.joinEUIClaimer(ctx, types.MustEUI64(in.JoinEui).OrZero()) + + dev, err := upstream.deviceRegistry.Get(ctx, &ttnpb.GetEndDeviceRequest{ + EndDeviceIds: in, + }) + if err != nil { + return nil, err + } + + if dev.GetIds().DevEui == nil || dev.GetIds().JoinEui == nil { + return nil, errNoEUI.New() + } + + joinEUI := types.MustEUI64(dev.GetIds().JoinEui).OrZero() + claimer := upstream.joinEUIClaimer(ctx, joinEUI) if claimer == nil { - return nil, errClaimingNotSupported.WithAttributes("eui", in.JoinEui) + return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) } - err = claimer.Unclaim(ctx, in) + err = claimer.Unclaim(ctx, dev.GetIds()) if err != nil { return nil, err } From 3155feea836917ec31d5ffa974b32d773e54d440 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Fri, 9 Jun 2023 12:29:53 +0200 Subject: [PATCH 2/8] dcs: Rearrange test files --- pkg/deviceclaimingserver/grpc_end_devices.go | 3 +- ...erver_test.go => grpc_end_devices_test.go} | 134 ++------------- pkg/deviceclaimingserver/grpc_gateways.go | 30 +++- .../grpc_gateways_test.go | 156 ++++++++++++++++++ 4 files changed, 193 insertions(+), 130 deletions(-) rename pkg/deviceclaimingserver/{deviceclaimingserver_test.go => grpc_end_devices_test.go} (58%) create mode 100644 pkg/deviceclaimingserver/grpc_gateways_test.go diff --git a/pkg/deviceclaimingserver/grpc_end_devices.go b/pkg/deviceclaimingserver/grpc_end_devices.go index 4962b4716d..d4b51f978f 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices.go +++ b/pkg/deviceclaimingserver/grpc_end_devices.go @@ -45,7 +45,8 @@ func (edcs *endDeviceClaimingServer) Claim( req *ttnpb.ClaimEndDeviceRequest, ) (*ttnpb.EndDeviceIdentifiers, error) { // Check that the collaborator has necessary rights before attempting to claim it on an upstream. - // Since this is part of the create device flow, we check that the collaborator has the rights to create devices in the application. + // Since this is part of the create device flow, + // we check that the collaborator has the rights to create devices in the application. targetAppID := req.GetTargetApplicationIds() if err := rights.RequireApplication(ctx, targetAppID, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, diff --git a/pkg/deviceclaimingserver/deviceclaimingserver_test.go b/pkg/deviceclaimingserver/grpc_end_devices_test.go similarity index 58% rename from pkg/deviceclaimingserver/deviceclaimingserver_test.go rename to pkg/deviceclaimingserver/grpc_end_devices_test.go index b873ba6c10..cbbe2cb741 100644 --- a/pkg/deviceclaimingserver/deviceclaimingserver_test.go +++ b/pkg/deviceclaimingserver/grpc_end_devices_test.go @@ -1,4 +1,4 @@ -// Copyright © 2021 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,15 +20,16 @@ import ( "time" "github.com/smartystreets/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/cluster" "go.thethings.network/lorawan-stack/v3/pkg/component" componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" "go.thethings.network/lorawan-stack/v3/pkg/config" . "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver" "go.thethings.network/lorawan-stack/v3/pkg/errors" + mockis "go.thethings.network/lorawan-stack/v3/pkg/identityserver/mock" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" - "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" "google.golang.org/grpc" @@ -55,8 +56,16 @@ func TestEndDeviceClaimingServer(t *testing.T) { cancelCtx() }) + is, isAddr, closeIS := mockis.New(ctx) + t.Cleanup(closeIS) + + _ = is + c := componenttest.NewComponent(t, &component.Config{ ServiceBase: config.ServiceBase{ + Cluster: cluster.Config{ + IdentityServer: isAddr, + }, GRPC: config.GRPC{ AllowInsecureForCredentials: true, }, @@ -172,124 +181,3 @@ func TestEndDeviceClaimingServer(t *testing.T) { }) } } - -var ( - claimAuthCode = []byte("test-code") - userID = ttnpb.UserIdentifiers{ - UserId: "test-user", - } - authorizedCallOpt = grpc.PerRPCCredentials(rpcmetadata.MD{ - AuthType: "Bearer", - AuthValue: "foo", - }) -) - -func TestGatewayClaimingServer(t *testing.T) { - t.Parallel() - a := assertions.New(t) - ctx := log.NewContext(test.Context(), test.GetLogger(t)) - ctx, cancelCtx := context.WithCancel(ctx) - t.Cleanup(func() { - cancelCtx() - }) - - c := componenttest.NewComponent(t, &component.Config{ - ServiceBase: config.ServiceBase{ - GRPC: config.GRPC{ - AllowInsecureForCredentials: true, - }, - }, - }) - test.Must(New(c, &Config{})) - componenttest.StartComponent(t, c) - t.Cleanup(func() { - c.Close() - }) - - // Wait for server to be ready. - time.Sleep(timeout) - - mustHavePeer(ctx, c, ttnpb.ClusterRole_DEVICE_CLAIMING_SERVER) - gclsClient := ttnpb.NewGatewayClaimingServerClient(c.LoopbackConn()) - - // Test API Validation here. Functionality is tested in the implementations. - for _, tc := range []struct { - Name string - Req any - ErrorAssertion func(err error) bool - }{ - { - Name: "Authorize/NilIDs", - Req: &ttnpb.AuthorizeGatewayRequest{ - GatewayIds: nil, - ApiKey: "test", - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Unauthorize/EmptyIDs", - Req: &ttnpb.GatewayIdentifiers{}, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/EmptyRequest", - Req: &ttnpb.ClaimGatewayRequest{ - Collaborator: userID.GetOrganizationOrUserIdentifiers(), - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/NilCollaborator", - Req: &ttnpb.ClaimGatewayRequest{ - Collaborator: nil, - SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ - AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ - GatewayEui: types.EUI64{0x58, 0xA0, 0xCB, 0xFF, 0xFE, 0x80, 0x00, 0x20}.Bytes(), - AuthenticationCode: claimAuthCode, - }, - }, - TargetGatewayId: "my-new-gateway", - TargetGatewayServerAddress: "target-tenant.things.example.com", - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/NilCollaborator", - Req: &ttnpb.ClaimGatewayRequest{ - Collaborator: nil, - SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ - AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ - GatewayEui: types.EUI64{0x58, 0xA0, 0xCB, 0xFF, 0xFE, 0x80, 0x00, 0x20}.Bytes(), - AuthenticationCode: claimAuthCode, - }, - }, - TargetGatewayId: "my-new-gateway", - TargetGatewayServerAddress: "target-tenant.things.example.com", - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - } { - tc := tc - t.Run(tc.Name, func(t *testing.T) { - t.Parallel() - var err error - switch req := tc.Req.(type) { - case *ttnpb.AuthorizeGatewayRequest: - _, err = gclsClient.AuthorizeGateway(ctx, req, authorizedCallOpt) - case *ttnpb.GatewayIdentifiers: - _, err = gclsClient.UnauthorizeGateway(ctx, req, authorizedCallOpt) - case *ttnpb.ClaimGatewayRequest: - _, err = gclsClient.Claim(ctx, req, authorizedCallOpt) - default: - panic("invalid request type") - } - if err != nil { - if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { - t.Fatalf("Unexpected error: %v", err) - } - } else if tc.ErrorAssertion != nil { - t.Fatalf("Expected error") - } - }) - } -} diff --git a/pkg/deviceclaimingserver/grpc_gateways.go b/pkg/deviceclaimingserver/grpc_gateways.go index cef5574899..7ecae5c0ae 100644 --- a/pkg/deviceclaimingserver/grpc_gateways.go +++ b/pkg/deviceclaimingserver/grpc_gateways.go @@ -27,17 +27,26 @@ type noopGCLS struct { } // Claim implements GatewayClaimingServer. -func (noopGCLS) Claim(ctx context.Context, req *ttnpb.ClaimGatewayRequest) (ids *ttnpb.GatewayIdentifiers, retErr error) { +func (noopGCLS) Claim( + _ context.Context, + _ *ttnpb.ClaimGatewayRequest, +) (ids *ttnpb.GatewayIdentifiers, retErr error) { return nil, errMethodUnavailable.New() } // AuthorizeGateway implements GatewayClaimingServer. -func (noopGCLS) AuthorizeGateway(ctx context.Context, req *ttnpb.AuthorizeGatewayRequest) (*emptypb.Empty, error) { +func (noopGCLS) AuthorizeGateway( + _ context.Context, + _ *ttnpb.AuthorizeGatewayRequest, +) (*emptypb.Empty, error) { return nil, errMethodUnavailable.New() } // UnauthorizeGateway implements GatewayClaimingServer. -func (noopGCLS) UnauthorizeGateway(ctx context.Context, gtwIDs *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { +func (noopGCLS) UnauthorizeGateway( + _ context.Context, + _ *ttnpb.GatewayIdentifiers, +) (*emptypb.Empty, error) { return nil, errMethodUnavailable.New() } @@ -49,16 +58,25 @@ type gatewayClaimingServer struct { } // Claim implements GatewayClaimingServer. -func (gcls gatewayClaimingServer) Claim(ctx context.Context, req *ttnpb.ClaimGatewayRequest) (ids *ttnpb.GatewayIdentifiers, retErr error) { +func (gcls gatewayClaimingServer) Claim( + ctx context.Context, + req *ttnpb.ClaimGatewayRequest, +) (ids *ttnpb.GatewayIdentifiers, retErr error) { return gcls.DCS.gatewayClaimingServerUpstream.Claim(ctx, req) } // AuthorizeGateway implements GatewayClaimingServer. -func (gcls gatewayClaimingServer) AuthorizeGateway(ctx context.Context, req *ttnpb.AuthorizeGatewayRequest) (*emptypb.Empty, error) { +func (gcls gatewayClaimingServer) AuthorizeGateway( + ctx context.Context, + req *ttnpb.AuthorizeGatewayRequest, +) (*emptypb.Empty, error) { return gcls.DCS.gatewayClaimingServerUpstream.AuthorizeGateway(ctx, req) } // UnauthorizeGateway implements GatewayClaimingServer. -func (gcls gatewayClaimingServer) UnauthorizeGateway(ctx context.Context, gtwIDs *ttnpb.GatewayIdentifiers) (*emptypb.Empty, error) { +func (gcls gatewayClaimingServer) UnauthorizeGateway( + ctx context.Context, + gtwIDs *ttnpb.GatewayIdentifiers, +) (*emptypb.Empty, error) { return gcls.DCS.gatewayClaimingServerUpstream.UnauthorizeGateway(ctx, gtwIDs) } diff --git a/pkg/deviceclaimingserver/grpc_gateways_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go new file mode 100644 index 0000000000..8724b7e2d0 --- /dev/null +++ b/pkg/deviceclaimingserver/grpc_gateways_test.go @@ -0,0 +1,156 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deviceclaimingserver_test + +import ( + "context" + "testing" + "time" + + "github.com/smartystreets/assertions" + "go.thethings.network/lorawan-stack/v3/pkg/component" + componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" + "go.thethings.network/lorawan-stack/v3/pkg/config" + . "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver" + "go.thethings.network/lorawan-stack/v3/pkg/errors" + "go.thethings.network/lorawan-stack/v3/pkg/log" + "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" + "go.thethings.network/lorawan-stack/v3/pkg/util/test" + "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" + "google.golang.org/grpc" +) + +var ( + claimAuthCode = []byte("test-code") + userID = ttnpb.UserIdentifiers{ + UserId: "test-user", + } + authorizedCallOpt = grpc.PerRPCCredentials(rpcmetadata.MD{ + AuthType: "Bearer", + AuthValue: "foo", + }) +) + +func TestGatewayClaimingServer(t *testing.T) { + t.Parallel() + a := assertions.New(t) + ctx := log.NewContext(test.Context(), test.GetLogger(t)) + ctx, cancelCtx := context.WithCancel(ctx) + t.Cleanup(func() { + cancelCtx() + }) + + c := componenttest.NewComponent(t, &component.Config{ + ServiceBase: config.ServiceBase{ + GRPC: config.GRPC{ + AllowInsecureForCredentials: true, + }, + }, + }) + test.Must(New(c, &Config{})) + componenttest.StartComponent(t, c) + t.Cleanup(func() { + c.Close() + }) + + // Wait for server to be ready. + time.Sleep(timeout) + + mustHavePeer(ctx, c, ttnpb.ClusterRole_DEVICE_CLAIMING_SERVER) + gclsClient := ttnpb.NewGatewayClaimingServerClient(c.LoopbackConn()) + + // Test API Validation here. Functionality is tested in the implementations. + for _, tc := range []struct { + Name string + Req any + ErrorAssertion func(err error) bool + }{ + { + Name: "Authorize/NilIDs", + Req: &ttnpb.AuthorizeGatewayRequest{ + GatewayIds: nil, + ApiKey: "test", + }, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "Unauthorize/EmptyIDs", + Req: &ttnpb.GatewayIdentifiers{}, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "Claim/EmptyRequest", + Req: &ttnpb.ClaimGatewayRequest{ + Collaborator: userID.GetOrganizationOrUserIdentifiers(), + }, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "Claim/NilCollaborator", + Req: &ttnpb.ClaimGatewayRequest{ + Collaborator: nil, + SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ + GatewayEui: types.EUI64{0x58, 0xA0, 0xCB, 0xFF, 0xFE, 0x80, 0x00, 0x20}.Bytes(), + AuthenticationCode: claimAuthCode, + }, + }, + TargetGatewayId: "my-new-gateway", + TargetGatewayServerAddress: "target-tenant.things.example.com", + }, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "Claim/NilCollaborator", + Req: &ttnpb.ClaimGatewayRequest{ + Collaborator: nil, + SourceGateway: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimGatewayRequest_AuthenticatedIdentifiers{ + GatewayEui: types.EUI64{0x58, 0xA0, 0xCB, 0xFF, 0xFE, 0x80, 0x00, 0x20}.Bytes(), + AuthenticationCode: claimAuthCode, + }, + }, + TargetGatewayId: "my-new-gateway", + TargetGatewayServerAddress: "target-tenant.things.example.com", + }, + ErrorAssertion: errors.IsInvalidArgument, + }, + } { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + var err error + switch req := tc.Req.(type) { + case *ttnpb.AuthorizeGatewayRequest: + _, err = gclsClient.AuthorizeGateway(ctx, req, authorizedCallOpt) + case *ttnpb.GatewayIdentifiers: + _, err = gclsClient.UnauthorizeGateway(ctx, req, authorizedCallOpt) + case *ttnpb.ClaimGatewayRequest: + _, err = gclsClient.Claim(ctx, req, authorizedCallOpt) + default: + panic("invalid request type") + } + if err != nil { + if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { + t.Fatalf("Unexpected error: %v", err) + } + } else if tc.ErrorAssertion != nil { + t.Fatalf("Expected error") + } + }) + } +} From b80497941a28ec5b3ded858f09b5741a323e1d22 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Fri, 9 Jun 2023 15:04:26 +0200 Subject: [PATCH 3/8] dcs: Refactor upstream --- .../enddevices/enddevices.go | 150 +----------------- pkg/deviceclaimingserver/grpc_end_devices.go | 93 ++++++++++- 2 files changed, 93 insertions(+), 150 deletions(-) diff --git a/pkg/deviceclaimingserver/enddevices/enddevices.go b/pkg/deviceclaimingserver/enddevices/enddevices.go index 45daff7e11..dceb10bcf3 100644 --- a/pkg/deviceclaimingserver/enddevices/enddevices.go +++ b/pkg/deviceclaimingserver/enddevices/enddevices.go @@ -20,20 +20,16 @@ import ( "path/filepath" "strings" - "go.thethings.network/lorawan-stack/v3/pkg/auth/rights" "go.thethings.network/lorawan-stack/v3/pkg/cluster" "go.thethings.network/lorawan-stack/v3/pkg/config" "go.thethings.network/lorawan-stack/v3/pkg/crypto" "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/enddevices/ttjsv2" - "go.thethings.network/lorawan-stack/v3/pkg/errors" "go.thethings.network/lorawan-stack/v3/pkg/fetch" "go.thethings.network/lorawan-stack/v3/pkg/httpclient" "go.thethings.network/lorawan-stack/v3/pkg/log" - "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" "google.golang.org/grpc" - "google.golang.org/protobuf/types/known/emptypb" "gopkg.in/yaml.v2" ) @@ -64,16 +60,13 @@ const ( // Upstream abstracts EndDeviceClaimingServer. type Upstream struct { - Component - deviceRegistry ttnpb.EndDeviceRegistryClient - servers map[string]EndDeviceClaimer + claimers map[string]EndDeviceClaimer } // NewUpstream returns a new Upstream. func NewUpstream(ctx context.Context, c Component, conf Config, opts ...Option) (*Upstream, error) { upstream := &Upstream{ - Component: c, - servers: make(map[string]EndDeviceClaimer), + claimers: make(map[string]EndDeviceClaimer), } fetcher, err := conf.Fetcher(ctx, c.GetBaseConfig(ctx).Blob, c) @@ -130,7 +123,7 @@ func NewUpstream(ctx context.Context, c Component, conf Config, opts ...Option) // The file for each client will be unique. clientName := strings.Trim(fileName, filepath.Ext(fileName)) - upstream.servers[clientName] = claimer + upstream.claimers[clientName] = claimer } for _, opt := range opts { @@ -143,139 +136,12 @@ func NewUpstream(ctx context.Context, c Component, conf Config, opts ...Option) // Option configures Upstream. type Option func(*Upstream) -// WithDeviceRegistry overrides the device registry of the Upstream. -func WithDeviceRegistry(reg ttnpb.EndDeviceRegistryClient) Option { - return func(upstream *Upstream) { - upstream.deviceRegistry = reg - } -} - -var ( - errNoEUI = errors.DefineInvalidArgument("no_eui", "DevEUI/JoinEUI not set for device") - errClaimingNotSupported = errors.DefineAborted("claiming_not_supported", "claiming not supported for JoinEUI `{eui}`") -) - -func (upstream *Upstream) joinEUIClaimer(_ context.Context, joinEUI types.EUI64) EndDeviceClaimer { - for _, srv := range upstream.servers { - if srv.SupportsJoinEUI(joinEUI) { - return srv +// JoinEUIClaimer returns the EndDeviceClaimer for the given JoinEUI. +func (upstream *Upstream) JoinEUIClaimer(_ context.Context, joinEUI types.EUI64) EndDeviceClaimer { + for _, claimer := range upstream.claimers { + if claimer.SupportsJoinEUI(joinEUI) { + return claimer } } return nil } - -// Claim implements EndDeviceClaimingServer. -func (upstream *Upstream) Claim( - ctx context.Context, joinEUI, devEUI types.EUI64, claimAuthenticationCode string, -) error { - claimer := upstream.joinEUIClaimer(ctx, joinEUI) - if claimer == nil { - return errClaimingNotSupported.WithAttributes("eui", joinEUI) - } - return claimer.Claim(ctx, joinEUI, devEUI, claimAuthenticationCode) -} - -// Unclaim implements EndDeviceClaimingServer. -func (upstream *Upstream) Unclaim(ctx context.Context, in *ttnpb.EndDeviceIdentifiers) (*emptypb.Empty, error) { - err := upstream.requireRights(ctx, in, &ttnpb.Rights{ - Rights: []ttnpb.Right{ - ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, - ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, - }, - }) - if err != nil { - return nil, err - } - - dev, err := upstream.deviceRegistry.Get(ctx, &ttnpb.GetEndDeviceRequest{ - EndDeviceIds: in, - }) - if err != nil { - return nil, err - } - - if dev.GetIds().DevEui == nil || dev.GetIds().JoinEui == nil { - return nil, errNoEUI.New() - } - - joinEUI := types.MustEUI64(dev.GetIds().JoinEui).OrZero() - claimer := upstream.joinEUIClaimer(ctx, joinEUI) - if claimer == nil { - return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) - } - err = claimer.Unclaim(ctx, dev.GetIds()) - if err != nil { - return nil, err - } - return ttnpb.Empty, nil -} - -// GetInfoByJoinEUI implements EndDeviceClaimingServer. -func (upstream *Upstream) GetInfoByJoinEUI( - ctx context.Context, in *ttnpb.GetInfoByJoinEUIRequest, -) (*ttnpb.GetInfoByJoinEUIResponse, error) { - joinEUI := types.MustEUI64(in.JoinEui).OrZero() - claimer := upstream.joinEUIClaimer(ctx, joinEUI) - return &ttnpb.GetInfoByJoinEUIResponse{ - JoinEui: joinEUI.Bytes(), - SupportsClaiming: (claimer != nil), - }, nil -} - -// GetClaimStatus implements EndDeviceClaimingServer. -func (upstream *Upstream) GetClaimStatus( - ctx context.Context, in *ttnpb.EndDeviceIdentifiers, -) (*ttnpb.GetClaimStatusResponse, error) { - if in.DevEui == nil || in.JoinEui == nil { - return nil, errNoEUI.New() - } - err := upstream.requireRights(ctx, in, &ttnpb.Rights{ - Rights: []ttnpb.Right{ - ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, - }, - }) - if err != nil { - return nil, err - } - claimer := upstream.joinEUIClaimer(ctx, types.MustEUI64(in.JoinEui).OrZero()) - if claimer == nil { - return nil, errClaimingNotSupported.WithAttributes("eui", in.JoinEui) - } - return claimer.GetClaimStatus(ctx, in) -} - -func (upstream *Upstream) requireRights( - ctx context.Context, in *ttnpb.EndDeviceIdentifiers, appRights *ttnpb.Rights, -) error { - // Collaborator must have the required rights on the application. - if err := rights.RequireApplication(ctx, in.ApplicationIds, - appRights.Rights..., - ); err != nil { - return err - } - // Check that the device actually exists in the application. - // If the EUIs are set in the request, the IS also checks that they match the stored device. - callOpt, err := rpcmetadata.WithForwardedAuth(ctx, upstream.Component.AllowInsecureForCredentials()) - if err != nil { - return err - } - er, err := upstream.getDeviceRegistry(ctx) - if err != nil { - return err - } - _, err = er.Get(ctx, &ttnpb.GetEndDeviceRequest{ - EndDeviceIds: in, - }, callOpt) - return err -} - -func (upstream *Upstream) getDeviceRegistry(ctx context.Context) (ttnpb.EndDeviceRegistryClient, error) { - if upstream.deviceRegistry != nil { - return upstream.deviceRegistry, nil - } - conn, err := upstream.Component.GetPeerConn(ctx, ttnpb.ClusterRole_ENTITY_REGISTRY, nil) - if err != nil { - return nil, err - } - return ttnpb.NewEndDeviceRegistryClient(conn), nil -} diff --git a/pkg/deviceclaimingserver/grpc_end_devices.go b/pkg/deviceclaimingserver/grpc_end_devices.go index d4b51f978f..9c84f0a276 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices.go +++ b/pkg/deviceclaimingserver/grpc_end_devices.go @@ -26,10 +26,15 @@ import ( ) var ( - errParseQRCode = errors.Define("parse_qr_code", "parse QR code failed") - errQRCodeData = errors.DefineInvalidArgument("qr_code_data", "invalid QR code data") - errNoJoinEUI = errors.DefineInvalidArgument("no_join_eui", "failed to extract JoinEUI from request") - errMethodUnavailable = errors.DefineUnimplemented("method_unavailable", "method unavailable") + errParseQRCode = errors.Define("parse_qr_code", "parse QR code failed") + errQRCodeData = errors.DefineInvalidArgument("qr_code_data", "invalid QR code data") + errNoJoinEUI = errors.DefineInvalidArgument("no_join_eui", "failed to extract JoinEUI from request") + errNoEUI = errors.DefineInvalidArgument("no_eui", "DevEUI/JoinEUI not set for device") + errMethodUnavailable = errors.DefineUnimplemented("method_unavailable", "method unavailable") + errClaimingNotSupported = errors.DefineAborted( + "claiming_not_supported", + "claiming not supported for JoinEUI `{eui}`", + ) ) // endDeviceClaimingServer is the front facing entity for gRPC requests. @@ -89,7 +94,16 @@ func (edcs *endDeviceClaimingServer) Claim( return nil, errNoJoinEUI.New() } - err := edcs.DCS.endDeviceClaimingUpstream.Claim(ctx, joinEUI, devEUI, claimAuthenticationCode) + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( + ctx, + joinEUI, + ) + + if claimer == nil { + return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) + } + + err := claimer.Claim(ctx, devEUI, joinEUI, claimAuthenticationCode) if err != nil { return nil, err } @@ -108,7 +122,29 @@ func (edcs *endDeviceClaimingServer) Unclaim( ctx context.Context, in *ttnpb.EndDeviceIdentifiers, ) (*emptypb.Empty, error) { - return edcs.DCS.endDeviceClaimingUpstream.Unclaim(ctx, in) + if in.DevEui == nil || in.JoinEui == nil { + return nil, errNoEUI.New() + } + + if err := rights.RequireApplication(ctx, in.GetApplicationIds(), + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + + joinEUI := types.MustEUI64(in.JoinEui).OrZero() + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( + ctx, + joinEUI, + ) + + if claimer == nil { + return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) + } + if err := claimer.Unclaim(ctx, in); err != nil { + return nil, err + } + return ttnpb.Empty, nil } // GetInfoByJoinEUI implements EndDeviceClaimingServer. @@ -116,7 +152,15 @@ func (edcs *endDeviceClaimingServer) GetInfoByJoinEUI( ctx context.Context, in *ttnpb.GetInfoByJoinEUIRequest, ) (*ttnpb.GetInfoByJoinEUIResponse, error) { - return edcs.DCS.endDeviceClaimingUpstream.GetInfoByJoinEUI(ctx, in) + joinEUI := types.MustEUI64(in.JoinEui).OrZero() + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( + ctx, + joinEUI, + ) + return &ttnpb.GetInfoByJoinEUIResponse{ + JoinEui: joinEUI.Bytes(), + SupportsClaiming: claimer != nil, + }, nil } // GetClaimStatus implements EndDeviceClaimingServer. @@ -124,5 +168,38 @@ func (edcs *endDeviceClaimingServer) GetClaimStatus( ctx context.Context, in *ttnpb.EndDeviceIdentifiers, ) (*ttnpb.GetClaimStatusResponse, error) { - return edcs.DCS.endDeviceClaimingUpstream.GetClaimStatus(ctx, in) + if err := rights.RequireApplication(ctx, in.GetApplicationIds(), + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ); err != nil { + return nil, err + } + if err := in.ValidateContext(ctx); err != nil { + return nil, err + } + + // Get the device from the Entity Registry. + conn, err := edcs.DCS.GetPeerConn(ctx, ttnpb.ClusterRole_ENTITY_REGISTRY, nil) + if err != nil { + return nil, err + } + reg := ttnpb.NewEndDeviceRegistryClient(conn) + + dev, err := reg.Get(ctx, &ttnpb.GetEndDeviceRequest{ + EndDeviceIds: in, + }) + if err != nil { + return nil, err + } + if dev.GetIds().DevEui == nil || dev.GetIds().JoinEui == nil { + return nil, errNoEUI.New() + } + joinEUI := types.MustEUI64(dev.GetIds().JoinEui).OrZero() + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( + ctx, + joinEUI, + ) + if claimer == nil { + return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) + } + return claimer.GetClaimStatus(ctx, dev.GetIds()) } From 3d101ca27775e669a94b8c1e119a9955b24a84ae Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Fri, 9 Jun 2023 15:22:13 +0200 Subject: [PATCH 4/8] dcs: Update upstream tests --- .../enddevices/enddevices_test.go | 74 +++---------------- pkg/deviceclaimingserver/grpc_end_devices.go | 23 +----- 2 files changed, 13 insertions(+), 84 deletions(-) diff --git a/pkg/deviceclaimingserver/enddevices/enddevices_test.go b/pkg/deviceclaimingserver/enddevices/enddevices_test.go index 91acbe074e..0e89e4a444 100644 --- a/pkg/deviceclaimingserver/enddevices/enddevices_test.go +++ b/pkg/deviceclaimingserver/enddevices/enddevices_test.go @@ -17,22 +17,14 @@ package enddevices import ( "testing" - "go.thethings.network/lorawan-stack/v3/pkg/auth/rights" "go.thethings.network/lorawan-stack/v3/pkg/component" componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" "go.thethings.network/lorawan-stack/v3/pkg/errors" - "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" "go.thethings.network/lorawan-stack/v3/pkg/types" - "go.thethings.network/lorawan-stack/v3/pkg/unique" "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" ) -var ( - supportedJoinEUI = &types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C} - unsupportedJoinEUI = &types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D} -) - func TestUpstream(t *testing.T) { t.Parallel() a, ctx := test.New(t) @@ -47,68 +39,20 @@ func TestUpstream(t *testing.T) { _, err := NewUpstream(ctx, c, Config{ Source: "directory", }) - a.So(err, should.NotBeNil) + a.So(errors.IsNotFound(err), should.BeTrue) - // Upstream test + // Test Upstream. upstream := test.Must(NewUpstream(ctx, c, Config{ NetID: test.DefaultNetID, Source: "directory", Directory: "testdata", - }, WithDeviceRegistry(&mockDeviceRegistry{}))) - - ctx = rights.NewContext(ctx, &rights.Rights{ - ApplicationRights: *rights.NewMap(map[string]*ttnpb.Rights{ - unique.ID(test.Context(), &ttnpb.ApplicationIdentifiers{ApplicationId: "test-app"}): ttnpb.RightsFrom( - ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, - ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, - ), - }), - }) - - // Invalid JoinEUI. - err = upstream.Claim(ctx, *unsupportedJoinEUI, - types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30}, - "secret", - ) - a.So(errors.IsAborted(err), should.BeTrue) - - _, err = upstream.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ - DeviceId: "test-dev", - ApplicationIds: &ttnpb.ApplicationIdentifiers{ - ApplicationId: "test-app", - }, - JoinEui: unsupportedJoinEUI.Bytes(), - DevEui: types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30}.Bytes(), - }) - a.So(errors.IsUnauthenticated(err), should.BeTrue) - - resp, err := upstream.GetInfoByJoinEUI(ctx, &ttnpb.GetInfoByJoinEUIRequest{ - JoinEui: unsupportedJoinEUI.Bytes(), - }) - a.So(err, should.BeNil) - a.So(resp.SupportsClaiming, should.BeFalse) + })) - // Valid JoinEUI. - inf, err := upstream.GetInfoByJoinEUI(ctx, &ttnpb.GetInfoByJoinEUIRequest{ - JoinEui: supportedJoinEUI.Bytes(), - }) - a.So(err, should.BeNil) - a.So(inf.JoinEui, should.Resemble, supportedJoinEUI.Bytes()) - a.So(inf.SupportsClaiming, should.BeTrue) - - err = upstream.Claim(ctx, *supportedJoinEUI, - types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30}, - "secret", - ) - a.So(!errors.IsUnimplemented(err), should.BeTrue) + unsupportedJoinEUI := types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D} + claimer := upstream.JoinEUIClaimer(ctx, unsupportedJoinEUI) + a.So(claimer, should.BeNil) - _, err = upstream.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ - DeviceId: "test-dev", - ApplicationIds: &ttnpb.ApplicationIdentifiers{ - ApplicationId: "test-app", - }, - JoinEui: supportedJoinEUI.Bytes(), - DevEui: types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30}.Bytes(), - }) - a.So(!errors.IsUnavailable(err), should.BeTrue) + supportedJoinEUI := types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C} + claimer = upstream.JoinEUIClaimer(ctx, supportedJoinEUI) + a.So(claimer, should.NotBeNil) } diff --git a/pkg/deviceclaimingserver/grpc_end_devices.go b/pkg/deviceclaimingserver/grpc_end_devices.go index 9c84f0a276..8a3de950c2 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices.go +++ b/pkg/deviceclaimingserver/grpc_end_devices.go @@ -94,11 +94,7 @@ func (edcs *endDeviceClaimingServer) Claim( return nil, errNoJoinEUI.New() } - claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( - ctx, - joinEUI, - ) - + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer(ctx, joinEUI) if claimer == nil { return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) } @@ -125,7 +121,6 @@ func (edcs *endDeviceClaimingServer) Unclaim( if in.DevEui == nil || in.JoinEui == nil { return nil, errNoEUI.New() } - if err := rights.RequireApplication(ctx, in.GetApplicationIds(), ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, ); err != nil { @@ -133,11 +128,7 @@ func (edcs *endDeviceClaimingServer) Unclaim( } joinEUI := types.MustEUI64(in.JoinEui).OrZero() - claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( - ctx, - joinEUI, - ) - + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer(ctx, joinEUI) if claimer == nil { return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) } @@ -153,10 +144,7 @@ func (edcs *endDeviceClaimingServer) GetInfoByJoinEUI( in *ttnpb.GetInfoByJoinEUIRequest, ) (*ttnpb.GetInfoByJoinEUIResponse, error) { joinEUI := types.MustEUI64(in.JoinEui).OrZero() - claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( - ctx, - joinEUI, - ) + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer(ctx, joinEUI) return &ttnpb.GetInfoByJoinEUIResponse{ JoinEui: joinEUI.Bytes(), SupportsClaiming: claimer != nil, @@ -194,10 +182,7 @@ func (edcs *endDeviceClaimingServer) GetClaimStatus( return nil, errNoEUI.New() } joinEUI := types.MustEUI64(dev.GetIds().JoinEui).OrZero() - claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer( - ctx, - joinEUI, - ) + claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer(ctx, joinEUI) if claimer == nil { return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) } From 128287251f927c4b4e1032e9636f5cd5e53966f1 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Mon, 12 Jun 2023 12:00:41 +0200 Subject: [PATCH 5/8] dcs: Add test cases --- .../deviceclaimingserver.go | 22 ++- .../enddevices/enddevices.go | 14 +- .../enddevices/util_test.go | 34 ---- .../grpc_end_devices_test.go | 146 ++++++++++++------ pkg/deviceclaimingserver/util_test.go | 54 +++++++ 5 files changed, 177 insertions(+), 93 deletions(-) delete mode 100644 pkg/deviceclaimingserver/enddevices/util_test.go create mode 100644 pkg/deviceclaimingserver/util_test.go diff --git a/pkg/deviceclaimingserver/deviceclaimingserver.go b/pkg/deviceclaimingserver/deviceclaimingserver.go index c31c018d44..0c6c626aa5 100644 --- a/pkg/deviceclaimingserver/deviceclaimingserver.go +++ b/pkg/deviceclaimingserver/deviceclaimingserver.go @@ -56,19 +56,18 @@ func New(c *component.Component, conf *Config, opts ...Option) (*DeviceClaimingS opt(dcs) } - dcs.gatewayClaimingServerUpstream = noopGCLS{} - - upstream, err := enddevices.NewUpstream(ctx, c, conf.EndDeviceClaimingServerConfig) - if err != nil { - return nil, err + if dcs.endDeviceClaimingUpstream == nil { + upstream, err := enddevices.NewUpstream(ctx, c, conf.EndDeviceClaimingServerConfig) + if err != nil { + return nil, err + } + dcs.endDeviceClaimingUpstream = upstream } - - dcs.endDeviceClaimingUpstream = upstream - dcs.grpc.endDeviceClaimingServer = &endDeviceClaimingServer{ DCS: dcs, } + dcs.gatewayClaimingServerUpstream = noopGCLS{} dcs.grpc.gatewayClaimingServer = &gatewayClaimingServer{ DCS: dcs, } @@ -80,6 +79,13 @@ func New(c *component.Component, conf *Config, opts ...Option) (*DeviceClaimingS // Option configures GatewayClaimingServer. type Option func(*DeviceClaimingServer) +// WithEndDeviceClaimingUpstream configures the upstream for end device claiming. +func WithEndDeviceClaimingUpstream(upstream *enddevices.Upstream) Option { + return func(dcs *DeviceClaimingServer) { + dcs.endDeviceClaimingUpstream = upstream + } +} + // Context returns the context of the Device Claiming Server. func (dcs *DeviceClaimingServer) Context() context.Context { return dcs.ctx diff --git a/pkg/deviceclaimingserver/enddevices/enddevices.go b/pkg/deviceclaimingserver/enddevices/enddevices.go index dceb10bcf3..9f8e484009 100644 --- a/pkg/deviceclaimingserver/enddevices/enddevices.go +++ b/pkg/deviceclaimingserver/enddevices/enddevices.go @@ -68,6 +68,9 @@ func NewUpstream(ctx context.Context, c Component, conf Config, opts ...Option) upstream := &Upstream{ claimers: make(map[string]EndDeviceClaimer), } + for _, opt := range opts { + opt(upstream) + } fetcher, err := conf.Fetcher(ctx, c.GetBaseConfig(ctx).Blob, c) if err != nil { @@ -126,16 +129,19 @@ func NewUpstream(ctx context.Context, c Component, conf Config, opts ...Option) upstream.claimers[clientName] = claimer } - for _, opt := range opts { - opt(upstream) - } - return upstream, nil } // Option configures Upstream. type Option func(*Upstream) +// WithClaimer adds a claimer to Upstream. +func WithClaimer(name string, claimer EndDeviceClaimer) Option { + return func(upstream *Upstream) { + upstream.claimers[name] = claimer + } +} + // JoinEUIClaimer returns the EndDeviceClaimer for the given JoinEUI. func (upstream *Upstream) JoinEUIClaimer(_ context.Context, joinEUI types.EUI64) EndDeviceClaimer { for _, claimer := range upstream.claimers { diff --git a/pkg/deviceclaimingserver/enddevices/util_test.go b/pkg/deviceclaimingserver/enddevices/util_test.go deleted file mode 100644 index cd4179adf0..0000000000 --- a/pkg/deviceclaimingserver/enddevices/util_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package enddevices - -import ( - "context" - - "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" - "google.golang.org/grpc" -) - -type mockDeviceRegistry struct { - ttnpb.EndDeviceRegistryClient -} - -func (mockDeviceRegistry) Get( - _ context.Context, in *ttnpb.GetEndDeviceRequest, _ ...grpc.CallOption, -) (*ttnpb.EndDevice, error) { - return &ttnpb.EndDevice{ - Ids: in.EndDeviceIds, - }, nil -} diff --git a/pkg/deviceclaimingserver/grpc_end_devices_test.go b/pkg/deviceclaimingserver/grpc_end_devices_test.go index cbbe2cb741..3eebb46f14 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices_test.go +++ b/pkg/deviceclaimingserver/grpc_end_devices_test.go @@ -25,11 +25,13 @@ import ( componenttest "go.thethings.network/lorawan-stack/v3/pkg/component/test" "go.thethings.network/lorawan-stack/v3/pkg/config" . "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver" + "go.thethings.network/lorawan-stack/v3/pkg/deviceclaimingserver/enddevices" "go.thethings.network/lorawan-stack/v3/pkg/errors" mockis "go.thethings.network/lorawan-stack/v3/pkg/identityserver/mock" "go.thethings.network/lorawan-stack/v3/pkg/log" "go.thethings.network/lorawan-stack/v3/pkg/rpcmetadata" "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" "go.thethings.network/lorawan-stack/v3/pkg/util/test" "go.thethings.network/lorawan-stack/v3/pkg/util/test/assertions/should" "google.golang.org/grpc" @@ -47,6 +49,17 @@ func mustHavePeer(ctx context.Context, c *component.Component, role ttnpb.Cluste panic("could not connect to peer") } +var ( + registeredApplicationIDs = &ttnpb.ApplicationIdentifiers{ + ApplicationId: "test-application", + } + registeredApplicationKey = "test-key" + registeredEndDeviceID = "test-end-device" + registeredJoinEUI = types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C} + unRegisteredJoinEUI = types.EUI64{0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D} + registeredDevEUI = types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30} +) + func TestEndDeviceClaimingServer(t *testing.T) { t.Parallel() a := assertions.New(t) @@ -59,8 +72,6 @@ func TestEndDeviceClaimingServer(t *testing.T) { is, isAddr, closeIS := mockis.New(ctx) t.Cleanup(closeIS) - _ = is - c := componenttest.NewComponent(t, &component.Config{ ServiceBase: config.ServiceBase{ Cluster: cluster.Config{ @@ -71,7 +82,18 @@ func TestEndDeviceClaimingServer(t *testing.T) { }, }, }) - test.Must(New(c, &Config{})) + mockUpstream, err := enddevices.NewUpstream( + ctx, + c, + enddevices.Config{}, + enddevices.WithClaimer("test", &MockClaimer{ + JoinEUI: registeredJoinEUI, + }), + ) + a.So(err, should.BeNil) + dcs, err := New(c, &Config{}, WithEndDeviceClaimingUpstream(mockUpstream)) + test.Must(dcs, err) + componenttest.StartComponent(t, c) t.Cleanup(func() { c.Close() @@ -83,49 +105,60 @@ func TestEndDeviceClaimingServer(t *testing.T) { mustHavePeer(ctx, c, ttnpb.ClusterRole_DEVICE_CLAIMING_SERVER) edcsClient := ttnpb.NewEndDeviceClaimingServerClient(c.LoopbackConn()) - ids := &ttnpb.ApplicationIdentifiers{ - ApplicationId: "foo", - } - - authorizedCallOpt := grpc.PerRPCCredentials(rpcmetadata.MD{ + authorizedCallOpt = grpc.PerRPCCredentials(rpcmetadata.MD{ AuthType: "Bearer", - AuthValue: "foo", + AuthValue: registeredApplicationKey, }) - // Test API Validation here. Functionality is tested in the implementations. + // Register entities. + is.ApplicationRegistry().Add( + ctx, + registeredApplicationIDs, + registeredApplicationKey, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ) + is.EndDeviceRegistry().Add( + ctx, + &ttnpb.EndDevice{ + Ids: &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + }, + }, + ) + + // GetInfoByJoinEUI. + resp, err := edcsClient.GetInfoByJoinEUI(ctx, &ttnpb.GetInfoByJoinEUIRequest{ + JoinEui: registeredJoinEUI.Bytes(), + }) + a.So(err, should.BeNil) + a.So(resp, should.NotBeNil) + a.So(resp.JoinEui, should.Resemble, registeredJoinEUI.Bytes()) + a.So(resp.SupportsClaiming, should.BeTrue) + + resp, err = edcsClient.GetInfoByJoinEUI(ctx, &ttnpb.GetInfoByJoinEUIRequest{ + JoinEui: unRegisteredJoinEUI.Bytes(), + }) + a.So(err, should.BeNil) + a.So(resp, should.NotBeNil) + a.So(resp.JoinEui, should.Resemble, unRegisteredJoinEUI.Bytes()) + a.So(resp.SupportsClaiming, should.BeFalse) + + // Claim end device. for _, tc := range []struct { Name string - Req any + Req *ttnpb.ClaimEndDeviceRequest ErrorAssertion func(err error) bool }{ { - Name: "Authorize/NilIDs", - Req: &ttnpb.AuthorizeApplicationRequest{ - ApplicationIds: nil, - ApiKey: "test", - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Authorize/EmptyAPIKey", - Req: &ttnpb.AuthorizeApplicationRequest{ - ApplicationIds: ids, - ApiKey: "", - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Unauthorize/EmptyAppIDs", - Req: &ttnpb.ApplicationIdentifiers{}, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/EmptyRequest", + Name: "EmptyRequest", Req: &ttnpb.ClaimEndDeviceRequest{}, ErrorAssertion: errors.IsInvalidArgument, }, { - Name: "Claim/NilTargetApplicationIds", + Name: "NilTargetApplicationIds", Req: &ttnpb.ClaimEndDeviceRequest{ SourceDevice: &ttnpb.ClaimEndDeviceRequest_QrCode{ QrCode: []byte("URN:LW:DP:42FFFFFFFFFFFFFF:4242FFFFFFFFFFFF:42FFFF42:%V0102"), @@ -135,7 +168,7 @@ func TestEndDeviceClaimingServer(t *testing.T) { ErrorAssertion: errors.IsInvalidArgument, }, { - Name: "Claim/NilSource", + Name: "NilSource", Req: &ttnpb.ClaimEndDeviceRequest{ SourceDevice: nil, TargetApplicationIds: &ttnpb.ApplicationIdentifiers{ @@ -145,7 +178,7 @@ func TestEndDeviceClaimingServer(t *testing.T) { ErrorAssertion: errors.IsInvalidArgument, }, { - Name: "Claim/NoEUIs", + Name: "NoEUIs", Req: &ttnpb.ClaimEndDeviceRequest{ SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{}, @@ -156,21 +189,25 @@ func TestEndDeviceClaimingServer(t *testing.T) { }, ErrorAssertion: errors.IsInvalidArgument, }, + { + Name: "ValidDevice", + Req: &ttnpb.ClaimEndDeviceRequest{ + SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{ + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + AuthenticationCode: "TEST1234", + }, + }, + TargetApplicationIds: registeredApplicationIDs, + TargetDeviceId: "target-device", + }, + }, } { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - var err error - switch req := tc.Req.(type) { - case *ttnpb.AuthorizeApplicationRequest: - _, err = edcsClient.AuthorizeApplication(ctx, req, authorizedCallOpt) - case *ttnpb.ApplicationIdentifiers: - _, err = edcsClient.UnauthorizeApplication(ctx, req, authorizedCallOpt) - case *ttnpb.ClaimEndDeviceRequest: - _, err = edcsClient.Claim(ctx, req, authorizedCallOpt) - default: - panic("invalid request type") - } + _, err := edcsClient.Claim(ctx, tc.Req, authorizedCallOpt) if err != nil { if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { t.Fatalf("Unexpected error: %v", err) @@ -180,4 +217,19 @@ func TestEndDeviceClaimingServer(t *testing.T) { } }) } + + // GetClaimStatus. + status, err := edcsClient.GetClaimStatus(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + }, authorizedCallOpt) + a.So(err, should.BeNil) + a.So(status, should.NotBeNil) + + // Unclaim. + _, err = edcsClient.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + }, authorizedCallOpt) + a.So(err, should.BeNil) } diff --git a/pkg/deviceclaimingserver/util_test.go b/pkg/deviceclaimingserver/util_test.go new file mode 100644 index 0000000000..ac4a4e9872 --- /dev/null +++ b/pkg/deviceclaimingserver/util_test.go @@ -0,0 +1,54 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deviceclaimingserver_test + +import ( + "context" + + "go.thethings.network/lorawan-stack/v3/pkg/ttnpb" + "go.thethings.network/lorawan-stack/v3/pkg/types" +) + +// MockClaimer is a mock Claimer. +type MockClaimer struct { + JoinEUI types.EUI64 +} + +// SupportsJoinEUI returns whether the Join Server supports this JoinEUI. +func (m MockClaimer) SupportsJoinEUI(joinEUI types.EUI64) bool { + return m.JoinEUI.Equal(joinEUI) +} + +// Claim claims an End Device. +func (MockClaimer) Claim(_ context.Context, _, _ types.EUI64, _ string, +) error { + return nil +} + +// GetClaimStatus returns the claim status an End Device. +func (MockClaimer) GetClaimStatus(_ context.Context, + ids *ttnpb.EndDeviceIdentifiers, +) (*ttnpb.GetClaimStatusResponse, error) { + return &ttnpb.GetClaimStatusResponse{ + EndDeviceIds: ids, + }, nil +} + +// Unclaim releases the claim on an End Device. +func (MockClaimer) Unclaim(_ context.Context, + _ *ttnpb.EndDeviceIdentifiers, +) (err error) { + return nil +} From 4233755922b3016cf4652721eb5f603376bef78c Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Mon, 12 Jun 2023 12:50:42 +0200 Subject: [PATCH 6/8] dev: Generate messages --- config/messages.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/config/messages.json b/config/messages.json index 702afc7415..a0cef65c2b 100644 --- a/config/messages.json +++ b/config/messages.json @@ -3644,27 +3644,27 @@ "file": "ttjs.go" } }, - "error:pkg/deviceclaimingserver/enddevices:claiming_not_supported": { + "error:pkg/deviceclaimingserver:claiming_not_supported": { "translations": { "en": "claiming not supported for JoinEUI `{eui}`" }, "description": { - "package": "pkg/deviceclaimingserver/enddevices", - "file": "enddevices.go" + "package": "pkg/deviceclaimingserver", + "file": "grpc_end_devices.go" } }, - "error:pkg/deviceclaimingserver/enddevices:no_eui": { + "error:pkg/deviceclaimingserver:method_unavailable": { "translations": { - "en": "DevEUI/JoinEUI not found in request" + "en": "method unavailable" }, "description": { - "package": "pkg/deviceclaimingserver/enddevices", - "file": "enddevices.go" + "package": "pkg/deviceclaimingserver", + "file": "grpc_end_devices.go" } }, - "error:pkg/deviceclaimingserver:method_unavailable": { + "error:pkg/deviceclaimingserver:no_eui": { "translations": { - "en": "method unavailable" + "en": "DevEUI/JoinEUI not set for device" }, "description": { "package": "pkg/deviceclaimingserver", From 65e763e8ea8d5924c7e5aa3bd1aee210ebfb3cda Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Mon, 12 Jun 2023 13:24:32 +0200 Subject: [PATCH 7/8] dcs: Update tests --- pkg/deviceclaimingserver/grpc_end_devices_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/deviceclaimingserver/grpc_end_devices_test.go b/pkg/deviceclaimingserver/grpc_end_devices_test.go index 3eebb46f14..39f285d070 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices_test.go +++ b/pkg/deviceclaimingserver/grpc_end_devices_test.go @@ -230,6 +230,8 @@ func TestEndDeviceClaimingServer(t *testing.T) { _, err = edcsClient.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ ApplicationIds: registeredApplicationIDs, DeviceId: registeredEndDeviceID, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), }, authorizedCallOpt) a.So(err, should.BeNil) } From 6b07c17b54870e5e0a6d0d295aa4146d359c1e03 Mon Sep 17 00:00:00 2001 From: Krishna Iyer Easwaran Date: Tue, 13 Jun 2023 10:28:14 +0200 Subject: [PATCH 8/8] dcs: Improve test coverage --- pkg/deviceclaimingserver/grpc_end_devices.go | 29 ++---- .../grpc_end_devices_test.go | 92 +++++++++++++++---- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/pkg/deviceclaimingserver/grpc_end_devices.go b/pkg/deviceclaimingserver/grpc_end_devices.go index 8a3de950c2..a0a0809dae 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices.go +++ b/pkg/deviceclaimingserver/grpc_end_devices.go @@ -156,35 +156,18 @@ func (edcs *endDeviceClaimingServer) GetClaimStatus( ctx context.Context, in *ttnpb.EndDeviceIdentifiers, ) (*ttnpb.GetClaimStatusResponse, error) { + if in.DevEui == nil || in.JoinEui == nil { + return nil, errNoEUI.New() + } if err := rights.RequireApplication(ctx, in.GetApplicationIds(), - ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, ); err != nil { return nil, err } - if err := in.ValidateContext(ctx); err != nil { - return nil, err - } - - // Get the device from the Entity Registry. - conn, err := edcs.DCS.GetPeerConn(ctx, ttnpb.ClusterRole_ENTITY_REGISTRY, nil) - if err != nil { - return nil, err - } - reg := ttnpb.NewEndDeviceRegistryClient(conn) - - dev, err := reg.Get(ctx, &ttnpb.GetEndDeviceRequest{ - EndDeviceIds: in, - }) - if err != nil { - return nil, err - } - if dev.GetIds().DevEui == nil || dev.GetIds().JoinEui == nil { - return nil, errNoEUI.New() - } - joinEUI := types.MustEUI64(dev.GetIds().JoinEui).OrZero() + joinEUI := types.MustEUI64(in.JoinEui).OrZero() claimer := edcs.DCS.endDeviceClaimingUpstream.JoinEUIClaimer(ctx, joinEUI) if claimer == nil { return nil, errClaimingNotSupported.WithAttributes("eui", joinEUI) } - return claimer.GetClaimStatus(ctx, dev.GetIds()) + return claimer.GetClaimStatus(ctx, in) } diff --git a/pkg/deviceclaimingserver/grpc_end_devices_test.go b/pkg/deviceclaimingserver/grpc_end_devices_test.go index 39f285d070..673e32f5f8 100644 --- a/pkg/deviceclaimingserver/grpc_end_devices_test.go +++ b/pkg/deviceclaimingserver/grpc_end_devices_test.go @@ -37,19 +37,9 @@ import ( "google.golang.org/grpc" ) -var timeout = (1 << 5) * test.Delay - -func mustHavePeer(ctx context.Context, c *component.Component, role ttnpb.ClusterRole) { - for i := 0; i < 20; i++ { - time.Sleep(20 * time.Millisecond) - if _, err := c.GetPeer(ctx, role, nil); err == nil { - return - } - } - panic("could not connect to peer") -} - var ( + timeout = (1 << 5) * test.Delay + registeredApplicationIDs = &ttnpb.ApplicationIdentifiers{ ApplicationId: "test-application", } @@ -60,6 +50,16 @@ var ( registeredDevEUI = types.EUI64{0x00, 0x04, 0xA3, 0x0B, 0x00, 0x1C, 0x05, 0x30} ) +func mustHavePeer(ctx context.Context, c *component.Component, role ttnpb.ClusterRole) { + for i := 0; i < 20; i++ { + time.Sleep(20 * time.Millisecond) + if _, err := c.GetPeer(ctx, role, nil); err == nil { + return + } + } + panic("could not connect to peer") +} + func TestEndDeviceClaimingServer(t *testing.T) { t.Parallel() a := assertions.New(t) @@ -95,9 +95,7 @@ func TestEndDeviceClaimingServer(t *testing.T) { test.Must(dcs, err) componenttest.StartComponent(t, c) - t.Cleanup(func() { - c.Close() - }) + t.Cleanup(c.Close) // Wait for server to be ready. time.Sleep(timeout) @@ -110,12 +108,18 @@ func TestEndDeviceClaimingServer(t *testing.T) { AuthValue: registeredApplicationKey, }) + unAuthorizedCallOpt := grpc.PerRPCCredentials(rpcmetadata.MD{ + AuthType: "Bearer", + AuthValue: "invalid-key", + }) + // Register entities. is.ApplicationRegistry().Add( ctx, registeredApplicationIDs, registeredApplicationKey, ttnpb.Right_RIGHT_APPLICATION_DEVICES_WRITE, + ttnpb.Right_RIGHT_APPLICATION_DEVICES_READ, ) is.EndDeviceRegistry().Add( ctx, @@ -150,11 +154,13 @@ func TestEndDeviceClaimingServer(t *testing.T) { for _, tc := range []struct { Name string Req *ttnpb.ClaimEndDeviceRequest + CallOpts grpc.CallOption ErrorAssertion func(err error) bool }{ { Name: "EmptyRequest", Req: &ttnpb.ClaimEndDeviceRequest{}, + CallOpts: authorizedCallOpt, ErrorAssertion: errors.IsInvalidArgument, }, { @@ -165,6 +171,7 @@ func TestEndDeviceClaimingServer(t *testing.T) { }, TargetApplicationIds: nil, }, + CallOpts: authorizedCallOpt, ErrorAssertion: errors.IsInvalidArgument, }, { @@ -175,6 +182,7 @@ func TestEndDeviceClaimingServer(t *testing.T) { ApplicationId: "target-app", }, }, + CallOpts: authorizedCallOpt, ErrorAssertion: errors.IsInvalidArgument, }, { @@ -187,8 +195,27 @@ func TestEndDeviceClaimingServer(t *testing.T) { ApplicationId: "target-app", }, }, + CallOpts: authorizedCallOpt, ErrorAssertion: errors.IsInvalidArgument, }, + { + Name: "PermissionDenied", + Req: &ttnpb.ClaimEndDeviceRequest{ + SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{ + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + AuthenticationCode: "TEST1234", + }, + }, + TargetApplicationIds: registeredApplicationIDs, + TargetDeviceId: "target-device", + }, + CallOpts: unAuthorizedCallOpt, + ErrorAssertion: func(err error) bool { + return errors.IsPermissionDenied(err) + }, + }, { Name: "ValidDevice", Req: &ttnpb.ClaimEndDeviceRequest{ @@ -202,12 +229,13 @@ func TestEndDeviceClaimingServer(t *testing.T) { TargetApplicationIds: registeredApplicationIDs, TargetDeviceId: "target-device", }, + CallOpts: authorizedCallOpt, }, } { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - _, err := edcsClient.Claim(ctx, tc.Req, authorizedCallOpt) + _, err := edcsClient.Claim(ctx, tc.Req, tc.CallOpts) if err != nil { if tc.ErrorAssertion == nil || !a.So(tc.ErrorAssertion(err), should.BeTrue) { t.Fatalf("Unexpected error: %v", err) @@ -223,10 +251,42 @@ func TestEndDeviceClaimingServer(t *testing.T) { ApplicationIds: registeredApplicationIDs, DeviceId: registeredEndDeviceID, }, authorizedCallOpt) + a.So(errors.IsInvalidArgument(err), should.BeTrue) + a.So(status, should.BeNil) + + status, err = edcsClient.GetClaimStatus(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + }, unAuthorizedCallOpt) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + a.So(status, should.BeNil) + + status, err = edcsClient.GetClaimStatus(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + }, authorizedCallOpt) a.So(err, should.BeNil) a.So(status, should.NotBeNil) // Unclaim. + _, err = edcsClient.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + }, authorizedCallOpt) + a.So(errors.IsInvalidArgument(err), should.BeTrue) + + _, err = edcsClient.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ + ApplicationIds: registeredApplicationIDs, + DeviceId: registeredEndDeviceID, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + }, unAuthorizedCallOpt) + a.So(errors.IsPermissionDenied(err), should.BeTrue) + _, err = edcsClient.Unclaim(ctx, &ttnpb.EndDeviceIdentifiers{ ApplicationIds: registeredApplicationIDs, DeviceId: registeredEndDeviceID,