@cryptotaxi247 / kubo / commits / 01cc5eab5

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 +}