feat(rpc): Opt-in HTTP RPC API Authorization (#10218)
Context: https://github.com/ipfs/kubo/issues/10187 Co-authored-by: Marcin Rataj <lidel@lidel.org>
Henrique Dias committed
Nov 17, 2023 at 01:29 UTC
01cc5eab57ed7acab1593280c7b5565d18c8f3ae
12 files changed
+463
-9
client/rpc/auth/auth.go
new
+29
@@ -0,0 +1,29 @@
1
+package auth
2
+
3
+import "net/http"
4
+
5
+var _ http.RoundTripper = &AuthorizedRoundTripper{}
6
+
7
+type AuthorizedRoundTripper struct {
8
+ authorization string
9
+ roundTripper http.RoundTripper
10
+}
11
+
12
+// NewAuthorizedRoundTripper creates a new [http.RoundTripper] that will set the
13
+// Authorization HTTP header with the value of [authorization]. The given [roundTripper] is
14
+// the base [http.RoundTripper]. If it is nil, [http.DefaultTransport] is used.
15
+func NewAuthorizedRoundTripper(authorization string, roundTripper http.RoundTripper) http.RoundTripper {
16
+ if roundTripper == nil {
17
+ roundTripper = http.DefaultTransport
18
+ }
19
+
20
+ return &AuthorizedRoundTripper{
21
+ authorization: authorization,
22
+ roundTripper: roundTripper,
23
+ }
24
+}
25
+
26
+func (tp *AuthorizedRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
27
+ r.Header.Set("Authorization", tp.authorization)
28
+ return tp.roundTripper.RoundTrip(r)
29
+}
cmd/ipfs/daemon.go
+4
@@ -676,6 +676,10 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
676
listeners = append(listeners, apiLis)
677
}
678
679
+ if len(cfg.API.Authorizations) > 0 && len(listeners) > 0 {
680
+ fmt.Printf("RPC API access is limited by the rules defined in API.Authorizations\n")
681
+ }
682
+
683
for _, listener := range listeners {
684
// we might have listened to /tcp/0 - let's see what we are listing on
685
fmt.Printf("RPC API server listening on %s\n", listener.Multiaddr())
cmd/ipfs/main.go
+8
@@ -23,8 +23,10 @@ import (
23
cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
24
logging "github.com/ipfs/go-log"
25
ipfs "github.com/ipfs/kubo"
26
+ "github.com/ipfs/kubo/client/rpc/auth"
27
"github.com/ipfs/kubo/cmd/ipfs/util"
28
oldcmds "github.com/ipfs/kubo/commands"
29
+ config "github.com/ipfs/kubo/config"
30
"github.com/ipfs/kubo/core"
31
corecmds "github.com/ipfs/kubo/core/commands"
32
"github.com/ipfs/kubo/core/corehttp"
@@ -325,6 +327,12 @@ func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
327
return nil, fmt.Errorf("unsupported API address: %s", apiAddr)
328
}
329
330
+ apiAuth, specified := req.Options[corecmds.ApiAuthOption].(string)
331
+ if specified {
332
+ authorization := config.ConvertAuthSecret(apiAuth)
333
+ tpt = auth.NewAuthorizedRoundTripper(authorization, tpt)
334
+ }
335
+
336
httpClient := &http.Client{
337
Transport: otelhttp.NewTransport(tpt),
338
}
config/api.go
+59
-1
@@ -1,5 +1,63 @@
1
package config
2
3
+import (
4
+ "encoding/base64"
5
+ "strings"
6
+)
7
+
8
+const (
9
+ APITag = "API"
10
+ AuthorizationTag = "Authorizations"
11
+)
12
+
13
+type RPCAuthScope struct {
14
+ // AuthSecret is the secret that will be compared to the HTTP "Authorization".
15
+ // header. A secret is in the format "type:value". Check the documentation for
16
+ // supported types.
17
+ AuthSecret string
18
+
19
+ // AllowedPaths is an explicit list of RPC path prefixes to allow.
20
+ // By default, none are allowed. ["/api/v0"] exposes all RPCs.
21
+ AllowedPaths []string
22
+}
23
+
24
type API struct {
4
- HTTPHeaders map[string][]string // HTTP headers to return with the API.
25
+ // HTTPHeaders are the HTTP headers to return with the API.
26
+ HTTPHeaders map[string][]string
27
+
28
+ // Authorization is a map of authorizations used to authenticate in the API.
29
+ // If the map is empty, then the RPC API is exposed to everyone. Check the
30
+ // documentation for more details.
31
+ Authorizations map[string]*RPCAuthScope `json:",omitempty"`
32
+}
33
+
34
+// ConvertAuthSecret converts the given secret in the format "type:value" into an
35
+// HTTP Authorization header value. It can handle 'bearer' and 'basic' as type.
36
+// If type exists and is not known, an empty string is returned. If type does not
37
+// exist, 'bearer' type is assumed.
38
+func ConvertAuthSecret(secret string) string {
39
+ if secret == "" {
40
+ return secret
41
+ }
42
+
43
+ split := strings.SplitN(secret, ":", 2)
44
+ if len(split) < 2 {
45
+ // No prefix: assume bearer token.
46
+ return "Bearer " + secret
47
+ }
48
+
49
+ if strings.HasPrefix(secret, "basic:") {
50
+ if strings.Contains(split[1], ":") {
51
+ // Assume basic:user:password
52
+ return "Basic " + base64.StdEncoding.EncodeToString([]byte(split[1]))
53
+ } else {
54
+ // Assume already base64 encoded.
55
+ return "Basic " + split[1]
56
+ }
57
+ } else if strings.HasPrefix(secret, "bearer:") {
58
+ return "Bearer " + split[1]
59
+ }
60
+
61
+ // Unknown. Type is present, but we can't handle it.
62
+ return ""
63
}
config/api_test.go
new
+22
@@ -0,0 +1,22 @@
1
+package config
2
+
3
+import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/assert"
7
+)
8
+
9
+func TestConvertAuthSecret(t *testing.T) {
10
+ for _, testCase := range []struct {
11
+ input string
12
+ output string
13
+ }{
14
+ {"", ""},
15
+ {"someToken", "Bearer someToken"},
16
+ {"bearer:someToken", "Bearer someToken"},
17
+ {"basic:user:pass", "Basic dXNlcjpwYXNz"},
18
+ {"basic:dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"},
19
+ } {
20
+ assert.Equal(t, testCase.output, ConvertAuthSecret(testCase.input))
21
+ }
22
+}
core/commands/config.go
+5
@@ -208,6 +208,11 @@ NOTE: For security reasons, this command will omit your private key and remote s
208
return err
209
}
210
211
+ cfg, err = scrubValue(cfg, []string{config.APITag, config.AuthorizationTag})
212
+ if err != nil {
213
+ return err
214
+ }
215
+
216
cfg, err = scrubOptionalValue(cfg, config.PinningConcealSelector)
217
if err != nil {
218
return err
core/commands/root.go
+3
-1
@@ -28,7 +28,8 @@ const (
28
DebugOption = "debug"
29
LocalOption = "local" // DEPRECATED: use OfflineOption
30
OfflineOption = "offline"
31
- ApiOption = "api" //nolint
31
+ ApiOption = "api" //nolint
32
+ ApiAuthOption = "api-auth" //nolint
33
)
34
35
var Root = &cmds.Command{
@@ -110,6 +111,7 @@ The CLI will exit with one of the following values:
111
cmds.BoolOption(LocalOption, "L", "Run the command locally, instead of using the daemon. DEPRECATED: use --offline."),
112
cmds.BoolOption(OfflineOption, "Run the command offline."),
113
cmds.StringOption(ApiOption, "Use a specific API instance (defaults to /ip4/127.0.0.1/tcp/5001)"),
114
+ cmds.StringOption(ApiAuthOption, "Optional RPC API authorization secret (defined as AuthSecret in API.Authorizations config)"),
115
116
// global options, added to every command
117
cmdenv.OptionCidBase,
core/corehttp/commands.go
+51
@@ -143,12 +143,63 @@ func commandsOption(cctx oldcmds.Context, command *cmds.Command, allowGet bool)
143
patchCORSVars(cfg, l.Addr())
144
145
cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
146
+
147
+ if len(rcfg.API.Authorizations) > 0 {
148
+ authorizations := convertAuthorizationsMap(rcfg.API.Authorizations)
149
+ cmdHandler = withAuthSecrets(authorizations, cmdHandler)
150
+ }
151
+
152
cmdHandler = otelhttp.NewHandler(cmdHandler, "corehttp.cmdsHandler")
153
mux.Handle(APIPath+"/", cmdHandler)
154
return mux, nil
155
}
156
}
157
158
+type rpcAuthScopeWithUser struct {
159
+ config.RPCAuthScope
160
+ User string
161
+}
162
+
163
+func convertAuthorizationsMap(authScopes map[string]*config.RPCAuthScope) map[string]rpcAuthScopeWithUser {
164
+ // authorizations is a map where we can just check for the header value to match.
165
+ authorizations := map[string]rpcAuthScopeWithUser{}
166
+ for user, authScope := range authScopes {
167
+ expectedHeader := config.ConvertAuthSecret(authScope.AuthSecret)
168
+ if expectedHeader != "" {
169
+ authorizations[expectedHeader] = rpcAuthScopeWithUser{
170
+ RPCAuthScope: *authScopes[user],
171
+ User: user,
172
+ }
173
+ }
174
+ }
175
+
176
+ return authorizations
177
+}
178
+
179
+func withAuthSecrets(authorizations map[string]rpcAuthScopeWithUser, next http.Handler) http.Handler {
180
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
181
+ authorizationHeader := r.Header.Get("Authorization")
182
+ auth, ok := authorizations[authorizationHeader]
183
+
184
+ if ok {
185
+ // version check is implicitly allowed
186
+ if r.URL.Path == "/api/v0/version" {
187
+ next.ServeHTTP(w, r)
188
+ return
189
+ }
190
+ // everything else has to be safelisted via AllowedPaths
191
+ for _, prefix := range auth.AllowedPaths {
192
+ if strings.HasPrefix(r.URL.Path, prefix) {
193
+ next.ServeHTTP(w, r)
194
+ return
195
+ }
196
+ }
197
+ }
198
+
199
+ http.Error(w, "Kubo RPC Access Denied: Please provide a valid authorization token as defined in the API.Authorizations configuration.", http.StatusForbidden)
200
+ })
201
+}
202
+
203
// CommandsOption constructs a ServerOption for hooking the commands into the
204
// HTTP server. It will NOT allow GET requests.
205
func CommandsOption(cctx oldcmds.Context) ServeOption {
docs/changelogs/v0.25.md
+14
@@ -6,6 +6,7 @@
6
7
- [Overview](#overview)
8
- [🔦 Highlights](#-highlights)
9
+ - [RPC `API.Authorizations`](#rpc-apiauthorizations)
10
- [📝 Changelog](#-changelog)
11
- [👨👩👧👦 Contributors](#-contributors)
12
@@ -13,6 +14,19 @@
14
15
### 🔦 Highlights
16
17
+#### RPC `API.Authorizations`
18
+
19
+Kubo RPC API now supports optional HTTP Authorization.
20
+
21
+Granular control over user access to the RPC can be defined in the
22
+[`API.Authorizations`](https://github.com/ipfs/kubo/blob/master/docs/config.md#apiauthorizations)
23
+map in the configuration file, allowing different users or apps to have unique
24
+access secrets and allowed paths.
25
+
26
+This feature is opt-in. By default, no authorization is set up.
27
+For configuration instructions,
28
+refer to the [documentation](https://github.com/ipfs/kubo/blob/master/docs/config.md#apiauthorizations).
29
+
30
### 📝 Changelog
31
32
### 👨👩👧👦 Contributors
docs/config.md
+84
@@ -28,6 +28,9 @@ config file at runtime.
28
- [`Addresses.NoAnnounce`](#addressesnoannounce)
29
- [`API`](#api)
30
- [`API.HTTPHeaders`](#apihttpheaders)
31
+ - [`API.Authorizations`](#apiauthorizations)
32
+ - [`API.Authorizations: AuthSecret`](#apiauthorizations-authsecret)
33
+ - [`API.Authorizations: AllowedPaths`](#apiauthorizations-allowedpaths)
34
- [`AutoNAT`](#autonat)
35
- [`AutoNAT.ServiceMode`](#autonatservicemode)
36
- [`AutoNAT.Throttle`](#autonatthrottle)
@@ -438,6 +441,87 @@ Default: `null`
441
442
Type: `object[string -> array[string]]` (header names -> array of header values)
443
444
+### `API.Authorizations`
445
+
446
+The `API.Authorizations` field defines user-based access restrictions for the
447
+[Kubo RPC API](https://docs.ipfs.tech/reference/kubo/rpc/), which is located at
448
+`Addresses.API` under `/api/v0` paths.
449
+
450
+By default, the RPC API is accessible without restrictions as it is only
451
+exposed on `127.0.0.1` and safeguarded with Origin check and implicit
452
+[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers that
453
+block random websites from accessing the RPC.
454
+
455
+When entries are defined in `API.Authorizations`, RPC requests will be declined
456
+unless a corresponding secret is present in the HTTP [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization),
457
+and the requested path is included in the `AllowedPaths` list for that specific
458
+secret.
459
+
460
+Default: `null`
461
+
462
+Type: `object[string -> object]` (user name -> authorization object, see bellow)
463
+
464
+For example, to limit RPC access to Alice (access `id` and MFS `files` commands with HTTP Basic Auth)
465
+and Bob (full access with Bearer token):
466
+
467
+```json
468
+{
469
+ "API": {
470
+ "Authorizations": {
471
+ "Alice": {
472
+ "AuthSecret": "basic:alice:password123",
473
+ "AllowedPaths": ["/api/v0/id", "/api/v0/files"]
474
+ },
475
+ "Bob": {
476
+ "AuthSecret": "bearer:secret-token123",
477
+ "AllowedPaths": ["/api/v0"]
478
+ }
479
+ }
480
+ }
481
+}
482
+
483
+```
484
+
485
+#### `API.Authorizations: AuthSecret`
486
+
487
+The `AuthSecret` field denotes the secret used by a user to authenticate,
488
+usually via HTTP [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization).
489
+
490
+Field format is `type:value`, and the following types are supported:
491
+
492
+- `bearer:` For secret Bearer tokens, set as `bearer:token`.
493
+ - If no known `type:` prefix is present, `bearer:` is assumed.
494
+- `basic`: For HTTP Basic Auth introduced in [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617). Value can be:
495
+ - `basic:user:pass`
496
+ - `basic:base64EncodedBasicAuth`
497
+
498
+One can use the config value for authentication via the command line:
499
+
500
+```
501
+ipfs id --api-auth basic:user:pass
502
+```
503
+
504
+Type: `string`
505
+
506
+#### `API.Authorizations: AllowedPaths`
507
+
508
+The `AllowedPaths` field is an array of strings containing allowed RPC path
509
+prefixes. Users authorized with the related `AuthSecret` will only be able to
510
+access paths prefixed by the specified prefixes.
511
+
512
+For instance:
513
+
514
+- If set to `["/api/v0"]`, the user will have access to the complete RPC API.
515
+- If set to `["/api/v0/id", "/api/v0/files"]`, the user will only have access
516
+ to the `id` command and all MFS commands under `files`.
517
+
518
+Note that `/api/v0/version` is always permitted access to allow version check
519
+to ensure compatibility.
520
+
521
+Default: `[]`
522
+
523
+Type: `array[string]`
524
+
525
## `AutoNAT`
526
527
Contains the configuration options for the AutoNAT service. The AutoNAT service
test/cli/harness/node.go
+22
-7
@@ -223,7 +223,7 @@ func (n *Node) Init(ipfsArgs ...string) *Node {
223
// harness.RunWithStdout(os.Stdout),
224
// },
225
// })
226
-func (n *Node) StartDaemonWithReq(req RunRequest) *Node {
226
+func (n *Node) StartDaemonWithReq(req RunRequest, authorization string) *Node {
227
alive := n.IsAlive()
228
if alive {
229
log.Panicf("node %d is already running", n.ID)
@@ -239,14 +239,20 @@ func (n *Node) StartDaemonWithReq(req RunRequest) *Node {
239
n.Daemon = res
240
241
log.Debugf("node %d started, checking API", n.ID)
242
- n.WaitOnAPI()
242
+ n.WaitOnAPI(authorization)
243
return n
244
}
245
246
func (n *Node) StartDaemon(ipfsArgs ...string) *Node {
247
return n.StartDaemonWithReq(RunRequest{
248
Args: ipfsArgs,
249
- })
249
+ }, "")
250
+}
251
+
252
+func (n *Node) StartDaemonWithAuthorization(secret string, ipfsArgs ...string) *Node {
253
+ return n.StartDaemonWithReq(RunRequest{
254
+ Args: ipfsArgs,
255
+ }, secret)
256
}
257
258
func (n *Node) signalAndWait(watch <-chan struct{}, signal os.Signal, t time.Duration) bool {
@@ -337,7 +343,7 @@ func (n *Node) TryAPIAddr() (multiaddr.Multiaddr, error) {
343
return ma, nil
344
}
345
340
-func (n *Node) checkAPI() bool {
346
+func (n *Node) checkAPI(authorization string) bool {
347
apiAddr, err := n.TryAPIAddr()
348
if err != nil {
349
log.Debugf("node %d API addr not available yet: %s", n.ID, err.Error())
@@ -353,7 +359,16 @@ func (n *Node) checkAPI() bool {
359
}
360
url := fmt.Sprintf("http://%s:%s/api/v0/id", ip, port)
361
log.Debugf("checking API for node %d at %s", n.ID, url)
356
- httpResp, err := http.Post(url, "", nil)
362
+
363
+ req, err := http.NewRequest(http.MethodPost, url, nil)
364
+ if err != nil {
365
+ panic(err)
366
+ }
367
+ if authorization != "" {
368
+ req.Header.Set("Authorization", authorization)
369
+ }
370
+
371
+ httpResp, err := http.DefaultClient.Do(req)
372
if err != nil {
373
log.Debugf("node %d API check error: %s", err.Error())
374
return false
@@ -402,10 +417,10 @@ func (n *Node) PeerID() peer.ID {
417
return id
418
}
419
405
-func (n *Node) WaitOnAPI() *Node {
420
+func (n *Node) WaitOnAPI(authorization string) *Node {
421
log.Debugf("waiting on API for node %d", n.ID)
422
for i := 0; i < 50; i++ {
408
- if n.checkAPI() {
423
+ if n.checkAPI(authorization) {
424
log.Debugf("daemon API found, daemon stdout: %s", n.Daemon.Stdout.String())
425
return n
426
}
test/cli/rpc_auth_test.go
new
+162
@@ -0,0 +1,162 @@
1
+package cli
2
+
3
+import (
4
+ "net/http"
5
+ "testing"
6
+
7
+ "github.com/ipfs/kubo/client/rpc/auth"
8
+ "github.com/ipfs/kubo/config"
9
+ "github.com/ipfs/kubo/test/cli/harness"
10
+ "github.com/stretchr/testify/assert"
11
+ "github.com/stretchr/testify/require"
12
+)
13
+
14
+const rpcDeniedMsg = "Kubo RPC Access Denied: Please provide a valid authorization token as defined in the API.Authorizations configuration."
15
+
16
+func TestRPCAuth(t *testing.T) {
17
+ t.Parallel()
18
+
19
+ makeAndStartProtectedNode := func(t *testing.T, authorizations map[string]*config.RPCAuthScope) *harness.Node {
20
+ authorizations["test-node-starter"] = &config.RPCAuthScope{
21
+ AuthSecret: "bearer:test-node-starter",
22
+ AllowedPaths: []string{"/api/v0"},
23
+ }
24
+
25
+ node := harness.NewT(t).NewNode().Init()
26
+ node.UpdateConfig(func(cfg *config.Config) {
27
+ cfg.API.Authorizations = authorizations
28
+ })
29
+ node.StartDaemonWithAuthorization("Bearer test-node-starter")
30
+ return node
31
+ }
32
+
33
+ makeHTTPTest := func(authSecret, header string) func(t *testing.T) {
34
+ return func(t *testing.T) {
35
+ t.Parallel()
36
+ t.Log(authSecret, header)
37
+
38
+ node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
39
+ "userA": {
40
+ AuthSecret: authSecret,
41
+ AllowedPaths: []string{"/api/v0/id"},
42
+ },
43
+ })
44
+
45
+ apiClient := node.APIClient()
46
+ apiClient.Client = &http.Client{
47
+ Transport: auth.NewAuthorizedRoundTripper(header, http.DefaultTransport),
48
+ }
49
+
50
+ // Can access /id with valid token
51
+ resp := apiClient.Post("/api/v0/id", nil)
52
+ assert.Equal(t, 200, resp.StatusCode)
53
+
54
+ // But not /config/show
55
+ resp = apiClient.Post("/api/v0/config/show", nil)
56
+ assert.Equal(t, 403, resp.StatusCode)
57
+
58
+ // create client which sends invalid access token
59
+ invalidApiClient := node.APIClient()
60
+ invalidApiClient.Client = &http.Client{
61
+ Transport: auth.NewAuthorizedRoundTripper("Bearer invalid", http.DefaultTransport),
62
+ }
63
+
64
+ // Can't access /id with invalid token
65
+ errResp := invalidApiClient.Post("/api/v0/id", nil)
66
+ assert.Equal(t, 403, errResp.StatusCode)
67
+
68
+ node.StopDaemon()
69
+ }
70
+ }
71
+
72
+ makeCLITest := func(authSecret string) func(t *testing.T) {
73
+ return func(t *testing.T) {
74
+ t.Parallel()
75
+
76
+ node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
77
+ "userA": {
78
+ AuthSecret: authSecret,
79
+ AllowedPaths: []string{"/api/v0/id"},
80
+ },
81
+ })
82
+
83
+ // Can access 'ipfs id'
84
+ resp := node.RunIPFS("id", "--api-auth", authSecret)
85
+ require.NoError(t, resp.Err)
86
+
87
+ // But not 'ipfs config show'
88
+ resp = node.RunIPFS("config", "show", "--api-auth", authSecret)
89
+ require.Error(t, resp.Err)
90
+ require.Contains(t, resp.Stderr.String(), rpcDeniedMsg)
91
+
92
+ node.StopDaemon()
93
+ }
94
+ }
95
+
96
+ for _, testCase := range []struct {
97
+ name string
98
+ authSecret string
99
+ header string
100
+ }{
101
+ {"Bearer (no type)", "myToken", "Bearer myToken"},
102
+ {"Bearer", "bearer:myToken", "Bearer myToken"},
103
+ {"Basic (user:pass)", "basic:user:pass", "Basic dXNlcjpwYXNz"},
104
+ {"Basic (encoded)", "basic:dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"},
105
+ } {
106
+ t.Run("AllowedPaths on CLI "+testCase.name, makeCLITest(testCase.authSecret))
107
+ t.Run("AllowedPaths on HTTP "+testCase.name, makeHTTPTest(testCase.authSecret, testCase.header))
108
+ }
109
+
110
+ t.Run("AllowedPaths set to /api/v0 Gives Full Access", func(t *testing.T) {
111
+ t.Parallel()
112
+
113
+ node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
114
+ "userA": {
115
+ AuthSecret: "bearer:userAToken",
116
+ AllowedPaths: []string{"/api/v0"},
117
+ },
118
+ })
119
+
120
+ apiClient := node.APIClient()
121
+ apiClient.Client = &http.Client{
122
+ Transport: auth.NewAuthorizedRoundTripper("Bearer userAToken", http.DefaultTransport),
123
+ }
124
+
125
+ resp := apiClient.Post("/api/v0/id", nil)
126
+ assert.Equal(t, 200, resp.StatusCode)
127
+
128
+ node.StopDaemon()
129
+ })
130
+
131
+ t.Run("API.Authorizations set to nil disables Authorization header check", func(t *testing.T) {
132
+ t.Parallel()
133
+
134
+ node := harness.NewT(t).NewNode().Init()
135
+ node.UpdateConfig(func(cfg *config.Config) {
136
+ cfg.API.Authorizations = nil
137
+ })
138
+ node.StartDaemon()
139
+
140
+ apiClient := node.APIClient()
141
+ resp := apiClient.Post("/api/v0/id", nil)
142
+ assert.Equal(t, 200, resp.StatusCode)
143
+
144
+ node.StopDaemon()
145
+ })
146
+
147
+ t.Run("API.Authorizations set to empty map disables Authorization header check", func(t *testing.T) {
148
+ t.Parallel()
149
+
150
+ node := harness.NewT(t).NewNode().Init()
151
+ node.UpdateConfig(func(cfg *config.Config) {
152
+ cfg.API.Authorizations = map[string]*config.RPCAuthScope{}
153
+ })
154
+ node.StartDaemon()
155
+
156
+ apiClient := node.APIClient()
157
+ resp := apiClient.Post("/api/v0/id", nil)
158
+ assert.Equal(t, 200, resp.StatusCode)
159
+
160
+ node.StopDaemon()
161
+ })
162
+}