master
go 285 lines 7.81 KB
Raw
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
163 t.Run("Requests without Authorization header are rejected when auth is enabled", func(t *testing.T) {
164 t.Parallel()
165
166 node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
167 "userA": {
168 AuthSecret: "bearer:mytoken",
169 AllowedPaths: []string{"/api/v0"},
170 },
171 })
172
173 // Create client with NO auth
174 apiClient := node.APIClient() // Uses http.DefaultClient with no auth headers
175
176 // Should be denied without auth header
177 resp := apiClient.Post("/api/v0/id", nil)
178 assert.Equal(t, 403, resp.StatusCode)
179
180 // Should contain denial message
181 assert.Contains(t, resp.Body, rpcDeniedMsg)
182
183 node.StopDaemon()
184 })
185
186 t.Run("Version endpoint is always accessible even with limited AllowedPaths", func(t *testing.T) {
187 t.Parallel()
188
189 node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
190 "userA": {
191 AuthSecret: "bearer:mytoken",
192 AllowedPaths: []string{"/api/v0/id"}, // Only /id allowed
193 },
194 })
195
196 apiClient := node.APIClient()
197 apiClient.Client = &http.Client{
198 Transport: auth.NewAuthorizedRoundTripper("Bearer mytoken", http.DefaultTransport),
199 }
200
201 // Can access /version even though not in AllowedPaths
202 resp := apiClient.Post("/api/v0/version", nil)
203 assert.Equal(t, 200, resp.StatusCode)
204
205 node.StopDaemon()
206 })
207
208 t.Run("User cannot access API with another user's secret", func(t *testing.T) {
209 t.Parallel()
210
211 node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
212 "alice": {
213 AuthSecret: "bearer:alice-secret",
214 AllowedPaths: []string{"/api/v0/id"},
215 },
216 "bob": {
217 AuthSecret: "bearer:bob-secret",
218 AllowedPaths: []string{"/api/v0/config"},
219 },
220 })
221
222 // Alice tries to use Bob's secret
223 apiClient := node.APIClient()
224 apiClient.Client = &http.Client{
225 Transport: auth.NewAuthorizedRoundTripper("Bearer bob-secret", http.DefaultTransport),
226 }
227
228 // Bob's secret should work for Bob's paths
229 resp := apiClient.Post("/api/v0/config/show", nil)
230 assert.Equal(t, 200, resp.StatusCode)
231
232 // But not for Alice's paths (Bob doesn't have access to /id)
233 resp = apiClient.Post("/api/v0/id", nil)
234 assert.Equal(t, 403, resp.StatusCode)
235
236 node.StopDaemon()
237 })
238
239 t.Run("Empty AllowedPaths denies all access except version", func(t *testing.T) {
240 t.Parallel()
241
242 node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
243 "userA": {
244 AuthSecret: "bearer:mytoken",
245 AllowedPaths: []string{}, // Empty!
246 },
247 })
248
249 apiClient := node.APIClient()
250 apiClient.Client = &http.Client{
251 Transport: auth.NewAuthorizedRoundTripper("Bearer mytoken", http.DefaultTransport),
252 }
253
254 // Should deny everything
255 resp := apiClient.Post("/api/v0/id", nil)
256 assert.Equal(t, 403, resp.StatusCode)
257
258 resp = apiClient.Post("/api/v0/config/show", nil)
259 assert.Equal(t, 403, resp.StatusCode)
260
261 // Except version
262 resp = apiClient.Post("/api/v0/version", nil)
263 assert.Equal(t, 200, resp.StatusCode)
264
265 node.StopDaemon()
266 })
267
268 t.Run("CLI commands fail without --api-auth when auth is enabled", func(t *testing.T) {
269 t.Parallel()
270
271 node := makeAndStartProtectedNode(t, map[string]*config.RPCAuthScope{
272 "userA": {
273 AuthSecret: "bearer:mytoken",
274 AllowedPaths: []string{"/api/v0"},
275 },
276 })
277
278 // Try to run command without --api-auth flag
279 resp := node.RunIPFS("id") // No --api-auth flag
280 require.Error(t, resp.Err)
281 require.Contains(t, resp.Stderr.String(), rpcDeniedMsg)
282
283 node.StopDaemon()
284 })
285 }