Skip to content

Commit 419720f

Browse files
committed
is: Rate limit entity creation by method and token
Previously, Create<Entity>Request was rate-limited per the entityID generated by the IDString interface, which was ineffective since each new entity has an unique ID. This allowed unlimited device creation attempts. Now rate limiting is applied withou the entityID, properly restricting create requests using full-method and auth-token strings as key.
1 parent 3d0d2f1 commit 419720f

12 files changed

Lines changed: 600 additions & 0 deletions

pkg/ttnpb/application_interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ func (m *CreateApplicationRequest) IDString() string {
7474
return m.GetApplication().IDString()
7575
}
7676

77+
// RateLimitKey is the implementation of the RateLimitKeyer interface.
78+
func (*CreateApplicationRequest) RateLimitKey() string {
79+
return ""
80+
}
81+
7782
func (m *UpdateApplicationRequest) IDString() string {
7883
return m.GetApplication().IDString()
7984
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ttnpb_test
16+
17+
import (
18+
"testing"
19+
20+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
21+
)
22+
23+
func TestCreateApplicationRequest_RateLimitKey(t *testing.T) {
24+
t.Parallel()
25+
tests := []struct {
26+
name string
27+
req *ttnpb.CreateApplicationRequest
28+
expected string
29+
}{
30+
{
31+
name: "valid request with user collaborator",
32+
req: &ttnpb.CreateApplicationRequest{
33+
Application: &ttnpb.Application{
34+
Ids: &ttnpb.ApplicationIdentifiers{
35+
ApplicationId: "test-app-1",
36+
},
37+
},
38+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
39+
Ids: &ttnpb.OrganizationOrUserIdentifiers_UserIds{
40+
UserIds: &ttnpb.UserIdentifiers{
41+
UserId: "test-user",
42+
},
43+
},
44+
},
45+
},
46+
expected: "",
47+
},
48+
{
49+
name: "different app same user",
50+
req: &ttnpb.CreateApplicationRequest{
51+
Application: &ttnpb.Application{
52+
Ids: &ttnpb.ApplicationIdentifiers{
53+
ApplicationId: "test-app-2",
54+
},
55+
},
56+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
57+
Ids: &ttnpb.OrganizationOrUserIdentifiers_UserIds{
58+
UserIds: &ttnpb.UserIdentifiers{
59+
UserId: "test-user",
60+
},
61+
},
62+
},
63+
},
64+
expected: "",
65+
},
66+
{
67+
name: "valid request with organization collaborator",
68+
req: &ttnpb.CreateApplicationRequest{
69+
Application: &ttnpb.Application{
70+
Ids: &ttnpb.ApplicationIdentifiers{
71+
ApplicationId: "test-app-3",
72+
},
73+
},
74+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
75+
Ids: &ttnpb.OrganizationOrUserIdentifiers_OrganizationIds{
76+
OrganizationIds: &ttnpb.OrganizationIdentifiers{
77+
OrganizationId: "test-org",
78+
},
79+
},
80+
},
81+
},
82+
expected: "",
83+
},
84+
{
85+
name: "nil collaborator",
86+
req: &ttnpb.CreateApplicationRequest{
87+
Application: &ttnpb.Application{
88+
Ids: &ttnpb.ApplicationIdentifiers{
89+
ApplicationId: "test-app",
90+
},
91+
},
92+
},
93+
expected: "",
94+
},
95+
{
96+
name: "nil request",
97+
req: &ttnpb.CreateApplicationRequest{},
98+
expected: "",
99+
},
100+
}
101+
102+
for _, tt := range tests {
103+
t.Run(tt.name, func(t *testing.T) {
104+
t.Parallel()
105+
got := tt.req.RateLimitKey()
106+
if got != tt.expected {
107+
t.Errorf("RateLimitKey() = %v, want %v", got, tt.expected)
108+
}
109+
})
110+
}
111+
}

pkg/ttnpb/client_interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ func (m *CreateClientRequest) IDString() string {
5858
return m.GetClient().IDString()
5959
}
6060

61+
// RateLimitKey is the implementation of the RateLimitKeyer interface.
62+
func (*CreateClientRequest) RateLimitKey() string {
63+
return ""
64+
}
65+
6166
func (m *UpdateClientRequest) IDString() string {
6267
return m.GetClient().IDString()
6368
}

pkg/ttnpb/client_ratelimit_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ttnpb_test
16+
17+
import (
18+
"testing"
19+
20+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
21+
)
22+
23+
func TestCreateClientRequest_RateLimitKey(t *testing.T) {
24+
t.Parallel()
25+
tests := []struct {
26+
name string
27+
req *ttnpb.CreateClientRequest
28+
expected string
29+
}{
30+
{
31+
name: "valid request with user collaborator",
32+
req: &ttnpb.CreateClientRequest{
33+
Client: &ttnpb.Client{
34+
Ids: &ttnpb.ClientIdentifiers{
35+
ClientId: "test-client-1",
36+
},
37+
},
38+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
39+
Ids: &ttnpb.OrganizationOrUserIdentifiers_UserIds{
40+
UserIds: &ttnpb.UserIdentifiers{
41+
UserId: "test-user",
42+
},
43+
},
44+
},
45+
},
46+
expected: "",
47+
},
48+
{
49+
name: "different client same user",
50+
req: &ttnpb.CreateClientRequest{
51+
Client: &ttnpb.Client{
52+
Ids: &ttnpb.ClientIdentifiers{
53+
ClientId: "test-client-2",
54+
},
55+
},
56+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
57+
Ids: &ttnpb.OrganizationOrUserIdentifiers_UserIds{
58+
UserIds: &ttnpb.UserIdentifiers{
59+
UserId: "test-user",
60+
},
61+
},
62+
},
63+
},
64+
expected: "",
65+
},
66+
{
67+
name: "valid request with organization collaborator",
68+
req: &ttnpb.CreateClientRequest{
69+
Client: &ttnpb.Client{
70+
Ids: &ttnpb.ClientIdentifiers{
71+
ClientId: "test-client-3",
72+
},
73+
},
74+
Collaborator: &ttnpb.OrganizationOrUserIdentifiers{
75+
Ids: &ttnpb.OrganizationOrUserIdentifiers_OrganizationIds{
76+
OrganizationIds: &ttnpb.OrganizationIdentifiers{
77+
OrganizationId: "test-org",
78+
},
79+
},
80+
},
81+
},
82+
expected: "",
83+
},
84+
{
85+
name: "nil collaborator",
86+
req: &ttnpb.CreateClientRequest{
87+
Client: &ttnpb.Client{
88+
Ids: &ttnpb.ClientIdentifiers{
89+
ClientId: "test-client",
90+
},
91+
},
92+
},
93+
expected: "",
94+
},
95+
{
96+
name: "nil request",
97+
req: &ttnpb.CreateClientRequest{},
98+
expected: "",
99+
},
100+
}
101+
102+
for _, tt := range tests {
103+
t.Run(tt.name, func(t *testing.T) {
104+
t.Parallel()
105+
got := tt.req.RateLimitKey()
106+
if got != tt.expected {
107+
t.Errorf("RateLimitKey() = %v, want %v", got, tt.expected)
108+
}
109+
})
110+
}
111+
}

pkg/ttnpb/end_device.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2990,6 +2990,11 @@ func (m *EndDevice) IDString() string {
29902990
return m.GetIds().IDString()
29912991
}
29922992

2993+
// RateLimitKey is the Implementation of the RateLimitKeyer interface.
2994+
func (*CreateEndDeviceRequest) RateLimitKey() string {
2995+
return ""
2996+
}
2997+
29932998
// All ExtractRequestFields methods are used by github.com/grpc-ecosystem/go-grpc-middleware/tags.
29942999

29953000
func (m *ResetAndGetEndDeviceRequest) ExtractRequestFields(dst map[string]interface{}) {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright © 2025 The Things Network Foundation, The Things Industries B.V.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ttnpb_test
16+
17+
import (
18+
"testing"
19+
20+
"go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
21+
)
22+
23+
func TestCreateEndDeviceRequest_RateLimitKey(t *testing.T) {
24+
t.Parallel()
25+
tests := []struct {
26+
name string
27+
req *ttnpb.CreateEndDeviceRequest
28+
expected string
29+
}{
30+
{
31+
name: "valid request",
32+
req: &ttnpb.CreateEndDeviceRequest{
33+
EndDevice: &ttnpb.EndDevice{
34+
Ids: &ttnpb.EndDeviceIdentifiers{
35+
ApplicationIds: &ttnpb.ApplicationIdentifiers{
36+
ApplicationId: "test-app",
37+
},
38+
DeviceId: "test-device-1",
39+
},
40+
},
41+
},
42+
expected: "",
43+
},
44+
{
45+
name: "different device same app",
46+
req: &ttnpb.CreateEndDeviceRequest{
47+
EndDevice: &ttnpb.EndDevice{
48+
Ids: &ttnpb.EndDeviceIdentifiers{
49+
ApplicationIds: &ttnpb.ApplicationIdentifiers{
50+
ApplicationId: "test-app",
51+
},
52+
DeviceId: "test-device-2",
53+
},
54+
},
55+
},
56+
expected: "",
57+
},
58+
{
59+
name: "nil request",
60+
req: &ttnpb.CreateEndDeviceRequest{},
61+
expected: "",
62+
},
63+
}
64+
65+
for _, tt := range tests {
66+
t.Run(tt.name, func(t *testing.T) {
67+
t.Parallel()
68+
got := tt.req.RateLimitKey()
69+
if got != tt.expected {
70+
t.Errorf("RateLimitKey() = %v, want %v", got, tt.expected)
71+
}
72+
})
73+
}
74+
}

pkg/ttnpb/gateway_interfaces.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ func (m *CreateGatewayRequest) IDString() string {
7474
return m.GetGateway().IDString()
7575
}
7676

77+
// RateLimitKey is the implementation of the RateLimitKeyer interface.
78+
func (*CreateGatewayRequest) RateLimitKey() string {
79+
return ""
80+
}
81+
7782
func (m *UpdateGatewayRequest) IDString() string {
7883
return m.GetGateway().IDString()
7984
}

0 commit comments

Comments
 (0)