kubo/test/cli/rpc_auth_test.go
Henrique Dias 01cc5eab57
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>
2023-11-17 01:29:29 +01:00

163 lines
4.5 KiB
Go

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()
})
}