Skip to content

Commit ac4581d

Browse files
torcolvinCopilot
andauthored
CBG-4687 have sgcollect_info use a token (#7604)
Co-authored-by: Copilot <[email protected]>
1 parent 910c3d3 commit ac4581d

File tree

11 files changed

+292
-167
lines changed

11 files changed

+292
-167
lines changed

base/devmode.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
const (
16-
assertionFailedPrefix = "Assertion failed: "
16+
AssertionFailedPrefix = "Assertion failed: "
1717
)
1818

1919
// IsDevMode returns true when compiled with the `cb_sg_devmode` build tag
@@ -26,5 +26,5 @@ func IsDevMode() bool {
2626
// Note: Callers MUST ensure code is safe to continue executing after the Assert (e.g. by returning an error) and MUST NOT be used like a panic that will halt.
2727
func AssertfCtx(ctx context.Context, format string, args ...any) {
2828
SyncGatewayStats.GlobalStats.ResourceUtilization.AssertionFailCount.Add(1)
29-
assertLogFn(ctx, assertionFailedPrefix+format, args...)
29+
assertLogFn(ctx, AssertionFailedPrefix+format, args...)
3030
}

base/main_test_bucket_pool.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@ func TestBucketPoolMain(ctx context.Context, m *testing.M, bucketReadierFunc TBP
675675

676676
teardownFuncs = append(teardownFuncs, func() {
677677
if numAssertionFails := SyncGatewayStats.GlobalStats.ResourceUtilizationStats().AssertionFailCount.Value(); numAssertionFails > 0 {
678-
panic(fmt.Sprintf("Test harness failed due to %d assertion failures. Search logs for %q", numAssertionFails, assertionFailedPrefix))
678+
panic(fmt.Sprintf("Test harness failed due to %d assertion failures. Search logs for %q", numAssertionFails, AssertionFailedPrefix))
679679
}
680680
})
681681
// must be the last teardown function added to the list to correctly detect leaked goroutines

rest/admin_api.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,9 +1673,6 @@ func (h *handler) handleSGCollect() error {
16731673
return base.HTTPErrorf(http.StatusBadRequest, "Invalid options used for sgcollect_info: %v", multiError)
16741674
}
16751675

1676-
// Populate username and password used by sgcollect_info script for talking to Sync Gateway.
1677-
params.syncGatewayUsername, params.syncGatewayPassword = h.getBasicAuth()
1678-
16791676
addr, err := h.server.getServerAddr(adminServer)
16801677
if err != nil {
16811678
return base.HTTPErrorf(http.StatusInternalServerError, "Error getting admin server address: %v", err)

rest/handler.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import (
3636
)
3737

3838
const (
39-
minCompressibleJSONSize = 1000
39+
minCompressibleJSONSize = 1000
40+
sgcollectTokenInvalidRequest = "sgcollect token auth present in non-sgcollect request"
4041
)
4142

4243
var _ http.Flusher = &CountedResponseWriter{}
@@ -111,6 +112,7 @@ type handler struct {
111112
httpLogLevel *base.LogLevel // if set, always log HTTP information at this level, instead of the default
112113
rqCtx context.Context
113114
serverType serverType
115+
sgcollect bool // is called by sgcollect
114116
}
115117

116118
type authScopeFunc func(context.Context, *handler) (string, error)
@@ -183,6 +185,7 @@ type handlerOptions struct {
183185
skipLogDuration bool // if true, will skip logging HTTP response status/duration
184186
authScopeFunc authScopeFunc // if set, this callback function will be used to set the auth scope for a given request body
185187
httpLogLevel *base.LogLevel // if set, log HTTP requests to this handler at this level, instead of the usual info level
188+
sgcollect bool // if true, this handler is being invoked as part of sgcollect
186189
}
187190

188191
func newHandler(server *ServerContext, privs handlerPrivs, serverType serverType, r http.ResponseWriter, rq *http.Request, options handlerOptions) *handler {
@@ -199,6 +202,7 @@ func newHandler(server *ServerContext, privs handlerPrivs, serverType serverType
199202
authScopeFunc: options.authScopeFunc,
200203
httpLogLevel: options.httpLogLevel,
201204
serverType: serverType,
205+
sgcollect: options.sgcollect,
202206
}
203207

204208
// initialize h.rqCtx
@@ -289,6 +293,23 @@ func (h *handler) invoke(method handlerMethod, accessPermissions []Permission, r
289293
return method(h) // Call the actual handler code
290294
}
291295

296+
// shouldCheckAdminRBAC returns true if the request needs to check the server for permissions to run
297+
func (h *handler) shouldCheckAdminRBAC() bool {
298+
sgcollectToken := h.server.SGCollect.getToken(h.rq.Header)
299+
if sgcollectToken != "" && h.sgcollect && h.server.SGCollect.hasValidToken(h.ctx(), sgcollectToken) {
300+
return false
301+
}
302+
if h.privs == adminPrivs && *h.server.Config.API.AdminInterfaceAuthentication {
303+
if sgcollectToken != "" && !h.sgcollect {
304+
base.AssertfCtx(h.ctx(), sgcollectTokenInvalidRequest)
305+
}
306+
return true
307+
} else if h.privs == metricsPrivs && *h.server.Config.API.MetricsInterfaceAuthentication {
308+
return true
309+
}
310+
return false
311+
}
312+
292313
// validateAndWriteHeaders sets up handler.db and validates the permission of the user and returns an error if there is not permission.
293314
func (h *handler) validateAndWriteHeaders(method handlerMethod, accessPermissions []Permission, responsePermissions []Permission) (err error) {
294315
var isRequestLogged bool
@@ -380,7 +401,7 @@ func (h *handler) validateAndWriteHeaders(method handlerMethod, accessPermission
380401

381402
// If an Admin Request and admin auth enabled or a metrics request with metrics auth enabled we need to check the
382403
// user credentials
383-
shouldCheckAdminAuth := (h.privs == adminPrivs && *h.server.Config.API.AdminInterfaceAuthentication) || (h.privs == metricsPrivs && *h.server.Config.API.MetricsInterfaceAuthentication)
404+
shouldCheckAdminAuth := h.shouldCheckAdminRBAC()
384405

385406
// If admin/metrics endpoint but auth not enabled, set admin_noauth log ctx
386407
if !shouldCheckAdminAuth && (h.serverType == adminServer || h.serverType == metricsServer) {

rest/handler_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ package rest
1313
import (
1414
"fmt"
1515
"net/http"
16+
"net/http/httptest"
1617
"testing"
1718

1819
"github.com/couchbase/sync_gateway/base"
1920
"github.com/stretchr/testify/assert"
21+
"github.com/stretchr/testify/require"
2022
)
2123

2224
func TestParseHTTPRangeHeader(t *testing.T) {
@@ -151,3 +153,72 @@ func Benchmark_parseKeyspace(b *testing.B) {
151153
_, _, _, _ = ParseKeyspace("d.s.c")
152154
}
153155
}
156+
157+
func TestShouldCheckAdminRBAC(t *testing.T) {
158+
for _, requireInterfaceAuth := range []bool{false, true} {
159+
t.Run(fmt.Sprintf("requireInterfaceAuth=%t", requireInterfaceAuth), func(t *testing.T) {
160+
config := BootstrapStartupConfigForTest(t)
161+
config.API.AdminInterfaceAuthentication = base.Ptr(requireInterfaceAuth)
162+
config.API.MetricsInterfaceAuthentication = base.Ptr(requireInterfaceAuth)
163+
sc, closeFn := StartServerWithConfig(t, &config)
164+
defer closeFn()
165+
166+
for _, sgcollectable := range []bool{true, false} {
167+
t.Run(fmt.Sprintf("sgcollectable=%t", sgcollectable), func(t *testing.T) {
168+
// make sure assertion counts are correct
169+
require.Equal(t, int64(0), base.SyncGatewayStats.GlobalStats.ResourceUtilizationStats().AssertionFailCount.Value())
170+
defer func() {
171+
base.SyncGatewayStats.GlobalStats.ResourceUtilizationStats().AssertionFailCount.Set(0)
172+
}()
173+
adminHandler := newHandler(sc, adminPrivs, adminServer, httptest.NewRecorder(), &http.Request{}, handlerOptions{sgcollect: sgcollectable})
174+
metricsHandler := newHandler(sc, metricsPrivs, metricsServer, httptest.NewRecorder(), &http.Request{}, handlerOptions{sgcollect: sgcollectable})
175+
if requireInterfaceAuth {
176+
require.True(t, adminHandler.shouldCheckAdminRBAC())
177+
require.True(t, metricsHandler.shouldCheckAdminRBAC())
178+
} else {
179+
require.False(t, adminHandler.shouldCheckAdminRBAC())
180+
181+
require.False(t, metricsHandler.shouldCheckAdminRBAC())
182+
}
183+
invalidAuthHeader := http.Header{}
184+
invalidAuthHeader.Add("Authorization", "SGCollect invalid")
185+
// with invalid sgcollect token
186+
adminHandler = newHandler(sc, adminPrivs, adminServer, httptest.NewRecorder(), &http.Request{Header: invalidAuthHeader}, handlerOptions{sgcollect: sgcollectable})
187+
metricsHandler = newHandler(sc, metricsPrivs, metricsServer, httptest.NewRecorder(), &http.Request{Header: invalidAuthHeader}, handlerOptions{sgcollect: sgcollectable})
188+
if requireInterfaceAuth {
189+
if base.IsDevMode() && !sgcollectable {
190+
require.PanicsWithValue(t, base.AssertionFailedPrefix+sgcollectTokenInvalidRequest, func() { adminHandler.shouldCheckAdminRBAC() })
191+
} else {
192+
require.True(t, adminHandler.shouldCheckAdminRBAC(), "expected invalid token to still require auth")
193+
}
194+
require.True(t, metricsHandler.shouldCheckAdminRBAC(), "expected invalid token to still require auth")
195+
} else {
196+
require.False(t, adminHandler.shouldCheckAdminRBAC())
197+
require.False(t, metricsHandler.shouldCheckAdminRBAC())
198+
199+
}
200+
// with valid sgcollect token, but sgcollect on the handler is disabled
201+
require.NoError(t, sc.SGCollect.createNewToken())
202+
203+
validAuthHeader := http.Header{}
204+
validAuthHeader.Add("Authorization", fmt.Sprintf("SGCollect %s", sc.SGCollect.Token))
205+
adminHandler = newHandler(sc, adminPrivs, adminServer, httptest.NewRecorder(), &http.Request{Header: validAuthHeader}, handlerOptions{sgcollect: false})
206+
metricsHandler = newHandler(sc, metricsPrivs, metricsServer, httptest.NewRecorder(), &http.Request{Header: validAuthHeader}, handlerOptions{sgcollect: false})
207+
if requireInterfaceAuth {
208+
if base.IsDevMode() {
209+
require.PanicsWithValue(t, base.AssertionFailedPrefix+sgcollectTokenInvalidRequest, func() { adminHandler.shouldCheckAdminRBAC() })
210+
} else {
211+
require.True(t, adminHandler.shouldCheckAdminRBAC(), "expected invalid token to still require auth")
212+
}
213+
require.True(t, metricsHandler.shouldCheckAdminRBAC(), "expected invalid token to still require auth")
214+
} else {
215+
require.False(t, adminHandler.shouldCheckAdminRBAC())
216+
217+
require.False(t, metricsHandler.shouldCheckAdminRBAC())
218+
}
219+
220+
})
221+
}
222+
})
223+
}
224+
}

rest/routing.go

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strconv"
1616
"strings"
1717

18+
"github.com/couchbase/sync_gateway/base"
1819
"github.com/gorilla/mux"
1920
)
2021

@@ -40,7 +41,7 @@ func createCommonRouter(sc *ServerContext, privs handlerPrivs, serverType server
4041
root = NewRouter(sc, serverType)
4142

4243
// Global operations:
43-
root.Handle("/", makeHandler(sc, privs, nil, nil, (*handler).handleRoot)).Methods("GET", "HEAD")
44+
root.Handle("/", makeHandlerWithOptions(sc, privs, nil, nil, (*handler).handleRoot, handlerOptions{sgcollect: true})).Methods("GET", "HEAD")
4445

4546
// Operations on databases:
4647
root.Handle("/{db:"+dbRegex+"}/", makeOfflineHandler(sc, privs, []Permission{PermDevOps, PermGetDb}, nil, (*handler).handleGetDB)).Methods("GET", "HEAD")
@@ -216,7 +217,7 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
216217
dbr.Handle("/_replicationStatus/{replicationID}",
217218
makeHandler(sc, adminPrivs, []Permission{PermWriteReplications}, nil, (*handler).putReplicationStatus)).Methods("PUT")
218219
dbr.Handle("/_config",
219-
makeOfflineHandler(sc, adminPrivs, []Permission{PermUpdateDb}, nil, (*handler).handleGetDbConfig)).Methods("GET")
220+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermUpdateDb}, nil, (*handler).handleGetDbConfig, handlerOptions{runOffline: true, sgcollect: true})).Methods("GET")
220221
dbr.Handle("/_config",
221222
makeOfflineHandler(sc, adminPrivs, []Permission{PermUpdateDb, PermConfigureSyncFn, PermConfigureAuth}, []Permission{PermUpdateDb, PermConfigureSyncFn, PermConfigureAuth}, (*handler).handlePutDbConfig)).Methods("PUT", "POST")
222223

@@ -260,15 +261,15 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
260261
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleSetLogging)).Methods("PUT", "POST")
261262

262263
r.Handle("/_config",
263-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetConfig)).Methods("GET")
264+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetConfig, handlerOptions{sgcollect: true})).Methods("GET")
264265
r.Handle("/_config",
265266
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePutConfig)).Methods("PUT")
266267

267268
r.Handle("/_cluster_info",
268-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetClusterInfo)).Methods("GET")
269+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetClusterInfo, handlerOptions{sgcollect: true})).Methods("GET")
269270

270271
r.Handle("/_status",
271-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetStatus)).Methods("GET")
272+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleGetStatus, handlerOptions{sgcollect: true})).Methods("GET")
272273

273274
r.Handle("/_sgcollect_info",
274275
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleSGCollectStatus)).Methods("GET")
@@ -281,33 +282,36 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
281282
r.Handle("/_stats",
282283
makeHandler(sc, adminPrivs, []Permission{PermStatsExport}, nil, (*handler).handleStats)).Methods("GET")
283284
r.Handle(kDebugURLPathPrefix,
284-
makeSilentHandler(sc, adminPrivs, []Permission{PermStatsExport}, nil, (*handler).handleExpvar)).Methods("GET")
285+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermStatsExport}, nil, (*handler).handleExpvar, handlerOptions{
286+
httpLogLevel: base.Ptr(base.LevelDebug), // silent handler
287+
sgcollect: true,
288+
})).Methods("GET")
285289
r.Handle("/_profile/{profilename}",
286-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleProfiling)).Methods("POST")
290+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleProfiling, handlerOptions{sgcollect: true})).Methods("POST")
287291
r.Handle("/_profile",
288-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleProfiling)).Methods("POST")
292+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleProfiling, handlerOptions{sgcollect: true})).Methods("POST")
289293
r.Handle("/_heap",
290-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleHeapProfiling)).Methods("POST")
294+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleHeapProfiling, handlerOptions{sgcollect: true})).Methods("POST")
291295
r.Handle("/_debug/pprof/goroutine",
292-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofGoroutine)).Methods("GET", "POST")
296+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofGoroutine, handlerOptions{sgcollect: true})).Methods("GET", "POST")
293297
r.Handle("/_debug/pprof/cmdline",
294-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofCmdline)).Methods("GET", "POST")
298+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofCmdline, handlerOptions{sgcollect: true})).Methods("GET", "POST")
295299
r.Handle("/_debug/pprof/symbol",
296-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofSymbol)).Methods("GET", "POST")
300+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofSymbol, handlerOptions{sgcollect: true})).Methods("GET", "POST")
297301
r.Handle("/_debug/pprof/heap",
298-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofHeap)).Methods("GET", "POST")
302+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofHeap, handlerOptions{sgcollect: true})).Methods("GET", "POST")
299303
r.Handle("/_debug/pprof/profile",
300-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofProfile)).Methods("GET", "POST")
304+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofProfile, handlerOptions{sgcollect: true})).Methods("GET", "POST")
301305
r.Handle("/_debug/pprof/block",
302-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofBlock)).Methods("GET", "POST")
306+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofBlock, handlerOptions{sgcollect: true})).Methods("GET", "POST")
303307
r.Handle("/_debug/pprof/threadcreate",
304-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofThreadcreate)).Methods("GET", "POST")
308+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofThreadcreate, handlerOptions{sgcollect: true})).Methods("GET", "POST")
305309
r.Handle("/_debug/pprof/mutex",
306-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofMutex)).Methods("GET", "POST")
310+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofMutex, handlerOptions{sgcollect: true})).Methods("GET", "POST")
307311
r.Handle("/_debug/pprof/trace",
308-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofTrace)).Methods("GET", "POST")
312+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePprofTrace, handlerOptions{sgcollect: true})).Methods("GET", "POST")
309313
r.Handle("/_debug/fgprof",
310-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleFgprof)).Methods("GET", "POST")
314+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleFgprof, handlerOptions{sgcollect: true})).Methods("GET", "POST")
311315

312316
r.Handle("/_post_upgrade",
313317
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handlePostUpgrade)).Methods("POST")
@@ -332,7 +336,7 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router {
332336
makeMetadataDBOfflineHandler(sc, adminPrivs, []Permission{PermDeleteDb}, nil, (*handler).handleDeleteDB)).Methods("DELETE")
333337

334338
r.Handle("/_all_dbs",
335-
makeHandler(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleAllDbs)).Methods("GET", "HEAD")
339+
makeHandlerWithOptions(sc, adminPrivs, []Permission{PermDevOps}, nil, (*handler).handleAllDbs, handlerOptions{sgcollect: true})).Methods("GET", "HEAD")
336340

337341
return r
338342
}

0 commit comments

Comments
 (0)