mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-17 06:57:40 +08:00
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>
This commit is contained in:
29
client/rpc/auth/auth.go
Normal file
29
client/rpc/auth/auth.go
Normal file
@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import "net/http"
|
||||
|
||||
var _ http.RoundTripper = &AuthorizedRoundTripper{}
|
||||
|
||||
type AuthorizedRoundTripper struct {
|
||||
authorization string
|
||||
roundTripper http.RoundTripper
|
||||
}
|
||||
|
||||
// NewAuthorizedRoundTripper creates a new [http.RoundTripper] that will set the
|
||||
// Authorization HTTP header with the value of [authorization]. The given [roundTripper] is
|
||||
// the base [http.RoundTripper]. If it is nil, [http.DefaultTransport] is used.
|
||||
func NewAuthorizedRoundTripper(authorization string, roundTripper http.RoundTripper) http.RoundTripper {
|
||||
if roundTripper == nil {
|
||||
roundTripper = http.DefaultTransport
|
||||
}
|
||||
|
||||
return &AuthorizedRoundTripper{
|
||||
authorization: authorization,
|
||||
roundTripper: roundTripper,
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *AuthorizedRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
r.Header.Set("Authorization", tp.authorization)
|
||||
return tp.roundTripper.RoundTrip(r)
|
||||
}
|
@ -676,6 +676,10 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
|
||||
listeners = append(listeners, apiLis)
|
||||
}
|
||||
|
||||
if len(cfg.API.Authorizations) > 0 && len(listeners) > 0 {
|
||||
fmt.Printf("RPC API access is limited by the rules defined in API.Authorizations\n")
|
||||
}
|
||||
|
||||
for _, listener := range listeners {
|
||||
// we might have listened to /tcp/0 - let's see what we are listing on
|
||||
fmt.Printf("RPC API server listening on %s\n", listener.Multiaddr())
|
||||
|
@ -23,8 +23,10 @@ import (
|
||||
cmdhttp "github.com/ipfs/go-ipfs-cmds/http"
|
||||
logging "github.com/ipfs/go-log"
|
||||
ipfs "github.com/ipfs/kubo"
|
||||
"github.com/ipfs/kubo/client/rpc/auth"
|
||||
"github.com/ipfs/kubo/cmd/ipfs/util"
|
||||
oldcmds "github.com/ipfs/kubo/commands"
|
||||
config "github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/core"
|
||||
corecmds "github.com/ipfs/kubo/core/commands"
|
||||
"github.com/ipfs/kubo/core/corehttp"
|
||||
@ -325,6 +327,12 @@ func makeExecutor(req *cmds.Request, env interface{}) (cmds.Executor, error) {
|
||||
return nil, fmt.Errorf("unsupported API address: %s", apiAddr)
|
||||
}
|
||||
|
||||
apiAuth, specified := req.Options[corecmds.ApiAuthOption].(string)
|
||||
if specified {
|
||||
authorization := config.ConvertAuthSecret(apiAuth)
|
||||
tpt = auth.NewAuthorizedRoundTripper(authorization, tpt)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: otelhttp.NewTransport(tpt),
|
||||
}
|
||||
|
@ -1,5 +1,63 @@
|
||||
package config
|
||||
|
||||
type API struct {
|
||||
HTTPHeaders map[string][]string // HTTP headers to return with the API.
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
APITag = "API"
|
||||
AuthorizationTag = "Authorizations"
|
||||
)
|
||||
|
||||
type RPCAuthScope struct {
|
||||
// AuthSecret is the secret that will be compared to the HTTP "Authorization".
|
||||
// header. A secret is in the format "type:value". Check the documentation for
|
||||
// supported types.
|
||||
AuthSecret string
|
||||
|
||||
// AllowedPaths is an explicit list of RPC path prefixes to allow.
|
||||
// By default, none are allowed. ["/api/v0"] exposes all RPCs.
|
||||
AllowedPaths []string
|
||||
}
|
||||
|
||||
type API struct {
|
||||
// HTTPHeaders are the HTTP headers to return with the API.
|
||||
HTTPHeaders map[string][]string
|
||||
|
||||
// Authorization is a map of authorizations used to authenticate in the API.
|
||||
// If the map is empty, then the RPC API is exposed to everyone. Check the
|
||||
// documentation for more details.
|
||||
Authorizations map[string]*RPCAuthScope `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ConvertAuthSecret converts the given secret in the format "type:value" into an
|
||||
// HTTP Authorization header value. It can handle 'bearer' and 'basic' as type.
|
||||
// If type exists and is not known, an empty string is returned. If type does not
|
||||
// exist, 'bearer' type is assumed.
|
||||
func ConvertAuthSecret(secret string) string {
|
||||
if secret == "" {
|
||||
return secret
|
||||
}
|
||||
|
||||
split := strings.SplitN(secret, ":", 2)
|
||||
if len(split) < 2 {
|
||||
// No prefix: assume bearer token.
|
||||
return "Bearer " + secret
|
||||
}
|
||||
|
||||
if strings.HasPrefix(secret, "basic:") {
|
||||
if strings.Contains(split[1], ":") {
|
||||
// Assume basic:user:password
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(split[1]))
|
||||
} else {
|
||||
// Assume already base64 encoded.
|
||||
return "Basic " + split[1]
|
||||
}
|
||||
} else if strings.HasPrefix(secret, "bearer:") {
|
||||
return "Bearer " + split[1]
|
||||
}
|
||||
|
||||
// Unknown. Type is present, but we can't handle it.
|
||||
return ""
|
||||
}
|
||||
|
22
config/api_test.go
Normal file
22
config/api_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertAuthSecret(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{"", ""},
|
||||
{"someToken", "Bearer someToken"},
|
||||
{"bearer:someToken", "Bearer someToken"},
|
||||
{"basic:user:pass", "Basic dXNlcjpwYXNz"},
|
||||
{"basic:dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"},
|
||||
} {
|
||||
assert.Equal(t, testCase.output, ConvertAuthSecret(testCase.input))
|
||||
}
|
||||
}
|
@ -208,6 +208,11 @@ NOTE: For security reasons, this command will omit your private key and remote s
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err = scrubValue(cfg, []string{config.APITag, config.AuthorizationTag})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err = scrubOptionalValue(cfg, config.PinningConcealSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -28,7 +28,8 @@ const (
|
||||
DebugOption = "debug"
|
||||
LocalOption = "local" // DEPRECATED: use OfflineOption
|
||||
OfflineOption = "offline"
|
||||
ApiOption = "api" //nolint
|
||||
ApiOption = "api" //nolint
|
||||
ApiAuthOption = "api-auth" //nolint
|
||||
)
|
||||
|
||||
var Root = &cmds.Command{
|
||||
@ -110,6 +111,7 @@ The CLI will exit with one of the following values:
|
||||
cmds.BoolOption(LocalOption, "L", "Run the command locally, instead of using the daemon. DEPRECATED: use --offline."),
|
||||
cmds.BoolOption(OfflineOption, "Run the command offline."),
|
||||
cmds.StringOption(ApiOption, "Use a specific API instance (defaults to /ip4/127.0.0.1/tcp/5001)"),
|
||||
cmds.StringOption(ApiAuthOption, "Optional RPC API authorization secret (defined as AuthSecret in API.Authorizations config)"),
|
||||
|
||||
// global options, added to every command
|
||||
cmdenv.OptionCidBase,
|
||||
|
@ -143,12 +143,63 @@ func commandsOption(cctx oldcmds.Context, command *cmds.Command, allowGet bool)
|
||||
patchCORSVars(cfg, l.Addr())
|
||||
|
||||
cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
|
||||
|
||||
if len(rcfg.API.Authorizations) > 0 {
|
||||
authorizations := convertAuthorizationsMap(rcfg.API.Authorizations)
|
||||
cmdHandler = withAuthSecrets(authorizations, cmdHandler)
|
||||
}
|
||||
|
||||
cmdHandler = otelhttp.NewHandler(cmdHandler, "corehttp.cmdsHandler")
|
||||
mux.Handle(APIPath+"/", cmdHandler)
|
||||
return mux, nil
|
||||
}
|
||||
}
|
||||
|
||||
type rpcAuthScopeWithUser struct {
|
||||
config.RPCAuthScope
|
||||
User string
|
||||
}
|
||||
|
||||
func convertAuthorizationsMap(authScopes map[string]*config.RPCAuthScope) map[string]rpcAuthScopeWithUser {
|
||||
// authorizations is a map where we can just check for the header value to match.
|
||||
authorizations := map[string]rpcAuthScopeWithUser{}
|
||||
for user, authScope := range authScopes {
|
||||
expectedHeader := config.ConvertAuthSecret(authScope.AuthSecret)
|
||||
if expectedHeader != "" {
|
||||
authorizations[expectedHeader] = rpcAuthScopeWithUser{
|
||||
RPCAuthScope: *authScopes[user],
|
||||
User: user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
func withAuthSecrets(authorizations map[string]rpcAuthScopeWithUser, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authorizationHeader := r.Header.Get("Authorization")
|
||||
auth, ok := authorizations[authorizationHeader]
|
||||
|
||||
if ok {
|
||||
// version check is implicitly allowed
|
||||
if r.URL.Path == "/api/v0/version" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// everything else has to be safelisted via AllowedPaths
|
||||
for _, prefix := range auth.AllowedPaths {
|
||||
if strings.HasPrefix(r.URL.Path, prefix) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.Error(w, "Kubo RPC Access Denied: Please provide a valid authorization token as defined in the API.Authorizations configuration.", http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
// CommandsOption constructs a ServerOption for hooking the commands into the
|
||||
// HTTP server. It will NOT allow GET requests.
|
||||
func CommandsOption(cctx oldcmds.Context) ServeOption {
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [🔦 Highlights](#-highlights)
|
||||
- [RPC `API.Authorizations`](#rpc-apiauthorizations)
|
||||
- [📝 Changelog](#-changelog)
|
||||
- [👨👩👧👦 Contributors](#-contributors)
|
||||
|
||||
@ -13,6 +14,19 @@
|
||||
|
||||
### 🔦 Highlights
|
||||
|
||||
#### RPC `API.Authorizations`
|
||||
|
||||
Kubo RPC API now supports optional HTTP Authorization.
|
||||
|
||||
Granular control over user access to the RPC can be defined in the
|
||||
[`API.Authorizations`](https://github.com/ipfs/kubo/blob/master/docs/config.md#apiauthorizations)
|
||||
map in the configuration file, allowing different users or apps to have unique
|
||||
access secrets and allowed paths.
|
||||
|
||||
This feature is opt-in. By default, no authorization is set up.
|
||||
For configuration instructions,
|
||||
refer to the [documentation](https://github.com/ipfs/kubo/blob/master/docs/config.md#apiauthorizations).
|
||||
|
||||
### 📝 Changelog
|
||||
|
||||
### 👨👩👧👦 Contributors
|
||||
|
@ -28,6 +28,9 @@ config file at runtime.
|
||||
- [`Addresses.NoAnnounce`](#addressesnoannounce)
|
||||
- [`API`](#api)
|
||||
- [`API.HTTPHeaders`](#apihttpheaders)
|
||||
- [`API.Authorizations`](#apiauthorizations)
|
||||
- [`API.Authorizations: AuthSecret`](#apiauthorizations-authsecret)
|
||||
- [`API.Authorizations: AllowedPaths`](#apiauthorizations-allowedpaths)
|
||||
- [`AutoNAT`](#autonat)
|
||||
- [`AutoNAT.ServiceMode`](#autonatservicemode)
|
||||
- [`AutoNAT.Throttle`](#autonatthrottle)
|
||||
@ -438,6 +441,87 @@ Default: `null`
|
||||
|
||||
Type: `object[string -> array[string]]` (header names -> array of header values)
|
||||
|
||||
### `API.Authorizations`
|
||||
|
||||
The `API.Authorizations` field defines user-based access restrictions for the
|
||||
[Kubo RPC API](https://docs.ipfs.tech/reference/kubo/rpc/), which is located at
|
||||
`Addresses.API` under `/api/v0` paths.
|
||||
|
||||
By default, the RPC API is accessible without restrictions as it is only
|
||||
exposed on `127.0.0.1` and safeguarded with Origin check and implicit
|
||||
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) headers that
|
||||
block random websites from accessing the RPC.
|
||||
|
||||
When entries are defined in `API.Authorizations`, RPC requests will be declined
|
||||
unless a corresponding secret is present in the HTTP [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization),
|
||||
and the requested path is included in the `AllowedPaths` list for that specific
|
||||
secret.
|
||||
|
||||
Default: `null`
|
||||
|
||||
Type: `object[string -> object]` (user name -> authorization object, see bellow)
|
||||
|
||||
For example, to limit RPC access to Alice (access `id` and MFS `files` commands with HTTP Basic Auth)
|
||||
and Bob (full access with Bearer token):
|
||||
|
||||
```json
|
||||
{
|
||||
"API": {
|
||||
"Authorizations": {
|
||||
"Alice": {
|
||||
"AuthSecret": "basic:alice:password123",
|
||||
"AllowedPaths": ["/api/v0/id", "/api/v0/files"]
|
||||
},
|
||||
"Bob": {
|
||||
"AuthSecret": "bearer:secret-token123",
|
||||
"AllowedPaths": ["/api/v0"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### `API.Authorizations: AuthSecret`
|
||||
|
||||
The `AuthSecret` field denotes the secret used by a user to authenticate,
|
||||
usually via HTTP [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization).
|
||||
|
||||
Field format is `type:value`, and the following types are supported:
|
||||
|
||||
- `bearer:` For secret Bearer tokens, set as `bearer:token`.
|
||||
- If no known `type:` prefix is present, `bearer:` is assumed.
|
||||
- `basic`: For HTTP Basic Auth introduced in [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617). Value can be:
|
||||
- `basic:user:pass`
|
||||
- `basic:base64EncodedBasicAuth`
|
||||
|
||||
One can use the config value for authentication via the command line:
|
||||
|
||||
```
|
||||
ipfs id --api-auth basic:user:pass
|
||||
```
|
||||
|
||||
Type: `string`
|
||||
|
||||
#### `API.Authorizations: AllowedPaths`
|
||||
|
||||
The `AllowedPaths` field is an array of strings containing allowed RPC path
|
||||
prefixes. Users authorized with the related `AuthSecret` will only be able to
|
||||
access paths prefixed by the specified prefixes.
|
||||
|
||||
For instance:
|
||||
|
||||
- If set to `["/api/v0"]`, the user will have access to the complete RPC API.
|
||||
- If set to `["/api/v0/id", "/api/v0/files"]`, the user will only have access
|
||||
to the `id` command and all MFS commands under `files`.
|
||||
|
||||
Note that `/api/v0/version` is always permitted access to allow version check
|
||||
to ensure compatibility.
|
||||
|
||||
Default: `[]`
|
||||
|
||||
Type: `array[string]`
|
||||
|
||||
## `AutoNAT`
|
||||
|
||||
Contains the configuration options for the AutoNAT service. The AutoNAT service
|
||||
|
@ -223,7 +223,7 @@ func (n *Node) Init(ipfsArgs ...string) *Node {
|
||||
// harness.RunWithStdout(os.Stdout),
|
||||
// },
|
||||
// })
|
||||
func (n *Node) StartDaemonWithReq(req RunRequest) *Node {
|
||||
func (n *Node) StartDaemonWithReq(req RunRequest, authorization string) *Node {
|
||||
alive := n.IsAlive()
|
||||
if alive {
|
||||
log.Panicf("node %d is already running", n.ID)
|
||||
@ -239,14 +239,20 @@ func (n *Node) StartDaemonWithReq(req RunRequest) *Node {
|
||||
n.Daemon = res
|
||||
|
||||
log.Debugf("node %d started, checking API", n.ID)
|
||||
n.WaitOnAPI()
|
||||
n.WaitOnAPI(authorization)
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *Node) StartDaemon(ipfsArgs ...string) *Node {
|
||||
return n.StartDaemonWithReq(RunRequest{
|
||||
Args: ipfsArgs,
|
||||
})
|
||||
}, "")
|
||||
}
|
||||
|
||||
func (n *Node) StartDaemonWithAuthorization(secret string, ipfsArgs ...string) *Node {
|
||||
return n.StartDaemonWithReq(RunRequest{
|
||||
Args: ipfsArgs,
|
||||
}, secret)
|
||||
}
|
||||
|
||||
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) {
|
||||
return ma, nil
|
||||
}
|
||||
|
||||
func (n *Node) checkAPI() bool {
|
||||
func (n *Node) checkAPI(authorization string) bool {
|
||||
apiAddr, err := n.TryAPIAddr()
|
||||
if err != nil {
|
||||
log.Debugf("node %d API addr not available yet: %s", n.ID, err.Error())
|
||||
@ -353,7 +359,16 @@ func (n *Node) checkAPI() bool {
|
||||
}
|
||||
url := fmt.Sprintf("http://%s:%s/api/v0/id", ip, port)
|
||||
log.Debugf("checking API for node %d at %s", n.ID, url)
|
||||
httpResp, err := http.Post(url, "", nil)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if authorization != "" {
|
||||
req.Header.Set("Authorization", authorization)
|
||||
}
|
||||
|
||||
httpResp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Debugf("node %d API check error: %s", err.Error())
|
||||
return false
|
||||
@ -402,10 +417,10 @@ func (n *Node) PeerID() peer.ID {
|
||||
return id
|
||||
}
|
||||
|
||||
func (n *Node) WaitOnAPI() *Node {
|
||||
func (n *Node) WaitOnAPI(authorization string) *Node {
|
||||
log.Debugf("waiting on API for node %d", n.ID)
|
||||
for i := 0; i < 50; i++ {
|
||||
if n.checkAPI() {
|
||||
if n.checkAPI(authorization) {
|
||||
log.Debugf("daemon API found, daemon stdout: %s", n.Daemon.Stdout.String())
|
||||
return n
|
||||
}
|
||||
|
162
test/cli/rpc_auth_test.go
Normal file
162
test/cli/rpc_auth_test.go
Normal file
@ -0,0 +1,162 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/kubo/client/rpc/auth"
|
||||
"github.com/ipfs/kubo/config"
|
||||
"github.com/ipfs/kubo/test/cli/harness"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const rpcDeniedMsg = "Kubo RPC Access Denied: Please provide a valid authorization token as defined in the API.Authorizations configuration."
|
||||
|
||||
func TestRPCAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
makeAndStartProtectedNode := func(t *testing.T, authorizations map[string]*config.RPCAuthScope) *harness.Node {
|
||||
authorizations["test-node-starter"] = &config.RPCAuthScope{
|
||||
AuthSecret: "bearer:test-node-starter",
|
||||
AllowedPaths: []string{"/api/v0"},
|
||||
}
|
||||
|
||||
node := harness.NewT(t).NewNode().Init()
|
||||
node.UpdateConfig(func(cfg *config.Config) {
|
||||
cfg.API.Authorizations = authorizations
|
||||
})
|
||||
node.StartDaemonWithAuthorization("Bearer test-node-starter")
|
||||
return node
|
||||
}
|
||||
|
||||
makeHTTPTest := func(authSecret, header string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Log(authSecret, header)
|
||||
|
||||
node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
|
||||
"userA": {
|
||||
AuthSecret: authSecret,
|
||||
AllowedPaths: []string{"/api/v0/id"},
|
||||
},
|
||||
})
|
||||
|
||||
apiClient := node.APIClient()
|
||||
apiClient.Client = &http.Client{
|
||||
Transport: auth.NewAuthorizedRoundTripper(header, http.DefaultTransport),
|
||||
}
|
||||
|
||||
// Can access /id with valid token
|
||||
resp := apiClient.Post("/api/v0/id", nil)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// But not /config/show
|
||||
resp = apiClient.Post("/api/v0/config/show", nil)
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
// create client which sends invalid access token
|
||||
invalidApiClient := node.APIClient()
|
||||
invalidApiClient.Client = &http.Client{
|
||||
Transport: auth.NewAuthorizedRoundTripper("Bearer invalid", http.DefaultTransport),
|
||||
}
|
||||
|
||||
// Can't access /id with invalid token
|
||||
errResp := invalidApiClient.Post("/api/v0/id", nil)
|
||||
assert.Equal(t, 403, errResp.StatusCode)
|
||||
|
||||
node.StopDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
makeCLITest := func(authSecret string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
|
||||
"userA": {
|
||||
AuthSecret: authSecret,
|
||||
AllowedPaths: []string{"/api/v0/id"},
|
||||
},
|
||||
})
|
||||
|
||||
// Can access 'ipfs id'
|
||||
resp := node.RunIPFS("id", "--api-auth", authSecret)
|
||||
require.NoError(t, resp.Err)
|
||||
|
||||
// But not 'ipfs config show'
|
||||
resp = node.RunIPFS("config", "show", "--api-auth", authSecret)
|
||||
require.Error(t, resp.Err)
|
||||
require.Contains(t, resp.Stderr.String(), rpcDeniedMsg)
|
||||
|
||||
node.StopDaemon()
|
||||
}
|
||||
}
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
authSecret string
|
||||
header string
|
||||
}{
|
||||
{"Bearer (no type)", "myToken", "Bearer myToken"},
|
||||
{"Bearer", "bearer:myToken", "Bearer myToken"},
|
||||
{"Basic (user:pass)", "basic:user:pass", "Basic dXNlcjpwYXNz"},
|
||||
{"Basic (encoded)", "basic:dXNlcjpwYXNz", "Basic dXNlcjpwYXNz"},
|
||||
} {
|
||||
t.Run("AllowedPaths on CLI "+testCase.name, makeCLITest(testCase.authSecret))
|
||||
t.Run("AllowedPaths on HTTP "+testCase.name, makeHTTPTest(testCase.authSecret, testCase.header))
|
||||
}
|
||||
|
||||
t.Run("AllowedPaths set to /api/v0 Gives Full Access", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
|
||||
"userA": {
|
||||
AuthSecret: "bearer:userAToken",
|
||||
AllowedPaths: []string{"/api/v0"},
|
||||
},
|
||||
})
|
||||
|
||||
apiClient := node.APIClient()
|
||||
apiClient.Client = &http.Client{
|
||||
Transport: auth.NewAuthorizedRoundTripper("Bearer userAToken", http.DefaultTransport),
|
||||
}
|
||||
|
||||
resp := apiClient.Post("/api/v0/id", nil)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
node.StopDaemon()
|
||||
})
|
||||
|
||||
t.Run("API.Authorizations set to nil disables Authorization header check", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := harness.NewT(t).NewNode().Init()
|
||||
node.UpdateConfig(func(cfg *config.Config) {
|
||||
cfg.API.Authorizations = nil
|
||||
})
|
||||
node.StartDaemon()
|
||||
|
||||
apiClient := node.APIClient()
|
||||
resp := apiClient.Post("/api/v0/id", nil)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
node.StopDaemon()
|
||||
})
|
||||
|
||||
t.Run("API.Authorizations set to empty map disables Authorization header check", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
node := harness.NewT(t).NewNode().Init()
|
||||
node.UpdateConfig(func(cfg *config.Config) {
|
||||
cfg.API.Authorizations = map[string]*config.RPCAuthScope{}
|
||||
})
|
||||
node.StartDaemon()
|
||||
|
||||
apiClient := node.APIClient()
|
||||
resp := apiClient.Post("/api/v0/id", nil)
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
node.StopDaemon()
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user