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", 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 f3ee6c7b52..9f8e484009 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,16 @@ 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), + } + for _, opt := range opts { + opt(upstream) } fetcher, err := conf.Fetcher(ctx, c.GetBaseConfig(ctx).Blob, c) @@ -130,11 +126,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 - } - - for _, opt := range opts { - opt(upstream) + upstream.claimers[clientName] = claimer } return upstream, nil @@ -143,128 +135,19 @@ 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 { +// WithClaimer adds a claimer to Upstream. +func WithClaimer(name string, claimer EndDeviceClaimer) Option { return func(upstream *Upstream) { - upstream.deviceRegistry = reg + upstream.claimers[name] = claimer } } -var ( - errNoEUI = errors.DefineInvalidArgument("no_eui", "DevEUI/JoinEUI not found in request") - 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) { - 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_WRITE, - }, - }) - 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) - } - err = claimer.Unclaim(ctx, in) - 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/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/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.go b/pkg/deviceclaimingserver/grpc_end_devices.go index 4962b4716d..a0a0809dae 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. @@ -45,7 +50,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, @@ -88,7 +94,12 @@ 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 } @@ -107,7 +118,24 @@ 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. @@ -115,7 +143,12 @@ 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. @@ -123,5 +156,18 @@ func (edcs *endDeviceClaimingServer) GetClaimStatus( ctx context.Context, in *ttnpb.EndDeviceIdentifiers, ) (*ttnpb.GetClaimStatusResponse, error) { - return edcs.DCS.endDeviceClaimingUpstream.GetClaimStatus(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_READ, + ); 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) + } + return claimer.GetClaimStatus(ctx, in) } diff --git a/pkg/deviceclaimingserver/grpc_end_devices_test.go b/pkg/deviceclaimingserver/grpc_end_devices_test.go new file mode 100644 index 0000000000..673e32f5f8 --- /dev/null +++ b/pkg/deviceclaimingserver/grpc_end_devices_test.go @@ -0,0 +1,297 @@ +// 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/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/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" +) + +var ( + timeout = (1 << 5) * test.Delay + + 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 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) + ctx := log.NewContext(test.Context(), test.GetLogger(t)) + ctx, cancelCtx := context.WithCancel(ctx) + t.Cleanup(func() { + cancelCtx() + }) + + is, isAddr, closeIS := mockis.New(ctx) + t.Cleanup(closeIS) + + c := componenttest.NewComponent(t, &component.Config{ + ServiceBase: config.ServiceBase{ + Cluster: cluster.Config{ + IdentityServer: isAddr, + }, + GRPC: config.GRPC{ + AllowInsecureForCredentials: true, + }, + }, + }) + 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(c.Close) + + // Wait for server to be ready. + time.Sleep(timeout) + + mustHavePeer(ctx, c, ttnpb.ClusterRole_DEVICE_CLAIMING_SERVER) + edcsClient := ttnpb.NewEndDeviceClaimingServerClient(c.LoopbackConn()) + + authorizedCallOpt = grpc.PerRPCCredentials(rpcmetadata.MD{ + AuthType: "Bearer", + 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, + &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 *ttnpb.ClaimEndDeviceRequest + CallOpts grpc.CallOption + ErrorAssertion func(err error) bool + }{ + { + Name: "EmptyRequest", + Req: &ttnpb.ClaimEndDeviceRequest{}, + CallOpts: authorizedCallOpt, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "NilTargetApplicationIds", + Req: &ttnpb.ClaimEndDeviceRequest{ + SourceDevice: &ttnpb.ClaimEndDeviceRequest_QrCode{ + QrCode: []byte("URN:LW:DP:42FFFFFFFFFFFFFF:4242FFFFFFFFFFFF:42FFFF42:%V0102"), + }, + TargetApplicationIds: nil, + }, + CallOpts: authorizedCallOpt, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "NilSource", + Req: &ttnpb.ClaimEndDeviceRequest{ + SourceDevice: nil, + TargetApplicationIds: &ttnpb.ApplicationIdentifiers{ + ApplicationId: "target-app", + }, + }, + CallOpts: authorizedCallOpt, + ErrorAssertion: errors.IsInvalidArgument, + }, + { + Name: "NoEUIs", + Req: &ttnpb.ClaimEndDeviceRequest{ + SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{}, + }, + TargetApplicationIds: &ttnpb.ApplicationIdentifiers{ + 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{ + SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ + AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{ + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + AuthenticationCode: "TEST1234", + }, + }, + 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, tc.CallOpts) + 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") + } + }) + } + + // GetClaimStatus. + status, err := edcsClient.GetClaimStatus(ctx, &ttnpb.EndDeviceIdentifiers{ + 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, + JoinEui: registeredJoinEUI.Bytes(), + DevEui: registeredDevEUI.Bytes(), + }, authorizedCallOpt) + a.So(err, should.BeNil) +} 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/deviceclaimingserver_test.go b/pkg/deviceclaimingserver/grpc_gateways_test.go similarity index 56% rename from pkg/deviceclaimingserver/deviceclaimingserver_test.go rename to pkg/deviceclaimingserver/grpc_gateways_test.go index b873ba6c10..8724b7e2d0 100644 --- a/pkg/deviceclaimingserver/deviceclaimingserver_test.go +++ b/pkg/deviceclaimingserver/grpc_gateways_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. @@ -34,145 +34,6 @@ 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") -} - -func TestEndDeviceClaimingServer(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) - edcsClient := ttnpb.NewEndDeviceClaimingServerClient(c.LoopbackConn()) - - ids := &ttnpb.ApplicationIdentifiers{ - ApplicationId: "foo", - } - - authorizedCallOpt := grpc.PerRPCCredentials(rpcmetadata.MD{ - AuthType: "Bearer", - AuthValue: "foo", - }) - - // 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.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", - Req: &ttnpb.ClaimEndDeviceRequest{}, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/NilTargetApplicationIds", - Req: &ttnpb.ClaimEndDeviceRequest{ - SourceDevice: &ttnpb.ClaimEndDeviceRequest_QrCode{ - QrCode: []byte("URN:LW:DP:42FFFFFFFFFFFFFF:4242FFFFFFFFFFFF:42FFFF42:%V0102"), - }, - TargetApplicationIds: nil, - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/NilSource", - Req: &ttnpb.ClaimEndDeviceRequest{ - SourceDevice: nil, - TargetApplicationIds: &ttnpb.ApplicationIdentifiers{ - ApplicationId: "target-app", - }, - }, - ErrorAssertion: errors.IsInvalidArgument, - }, - { - Name: "Claim/NoEUIs", - Req: &ttnpb.ClaimEndDeviceRequest{ - SourceDevice: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers_{ - AuthenticatedIdentifiers: &ttnpb.ClaimEndDeviceRequest_AuthenticatedIdentifiers{}, - }, - TargetApplicationIds: &ttnpb.ApplicationIdentifiers{ - ApplicationId: "target-app", - }, - }, - 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.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") - } - 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") - } - }) - } -} - var ( claimAuthCode = []byte("test-code") userID = ttnpb.UserIdentifiers{ 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 +}