Skip to content

CBG-4617: Add DisablePublicAllDocs db config option #7540

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions db/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ type DatabaseContextOptions struct {
NumIndexReplicas uint // Number of replicas for GSI indexes
NumIndexPartitions *uint32 // Number of partitions for GSI indexes, if not set will default to 1
ImportVersion uint64 // Version included in import DCP checkpoints, incremented when collections added to db
DisablePublicAllDocs bool // Disable public access to the _all_docs endpoint for this database
}

type ConfigPrincipals struct {
Expand Down
10 changes: 10 additions & 0 deletions docs/api/components/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,16 @@ Database:
name:
description: The name of the role for which audit logging is disabled.
type: string
disable_public_all_docs:
description: |-
This controls whether the [GET /{keyspace}/_all_docs](#operation/get_keyspace-_all_docs) REST API endpoint is publicly accessible or not.
Disabling this endpoint is recommended for larger datasets or production workloads.
[GET /{keyspace}/_changes](#operation/get_keyspace-_changes) or [POST /{keyspace}/_bulk_get](#operation/post_keyspace-_bulk_get) have more efficient implementations and should be used instead.

If set to `true`, the endpoint will not be publicly accessible, and will only be available on the Admin API.
Setting this to `false`, or leaving it as the default value is deprecated, and may default to `true` in a future release.
type: boolean
default: false
title: Database-config
Disabled-users-and-roles:
type: object
Expand Down
6 changes: 6 additions & 0 deletions docs/api/paths/admin/keyspace-_all_docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ get:
description: |-
Returns all documents in the database based on the specified parameters.

This endpoint is not recommended for larger datasets or production workloads.
[GET /{keyspace}/_changes](#operation/get_keyspace-_changes) or [POST /{keyspace}/_bulk_get](#operation/post_keyspace-_bulk_get) have more efficient implementations and should be used instead.

Required Sync Gateway RBAC roles:

* Sync Gateway Application
Expand Down Expand Up @@ -41,6 +44,9 @@ post:
description: |-
Returns all documents in the database based on the specified parameters.

This endpoint is not recommended for larger datasets or production workloads.
[GET /{keyspace}/_changes](#operation/get_keyspace-_changes) or [POST /{keyspace}/_bulk_get](#operation/post_keyspace-_bulk_get) have more efficient implementations and should be used instead.

Required Sync Gateway RBAC roles:

* Sync Gateway Application
Expand Down
24 changes: 22 additions & 2 deletions docs/api/paths/public/keyspace-_all_docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ parameters:
- $ref: ../../components/parameters.yaml#/keyspace
get:
summary: Gets all the documents in the database with the given parameters
description: Returns all documents in the database based on the specified parameters.
description: |-
Returns all documents in the database based on the specified parameters.

This endpoint is not recommended for larger datasets or production workloads.
[GET /{keyspace}/_changes](#operation/get_keyspace-_changes) or [POST /{keyspace}/_bulk_get](#operation/post_keyspace-_bulk_get) have more efficient implementations and should be used instead.
parameters:
- $ref: ../../components/parameters.yaml#/include_docs
- $ref: ../../components/parameters.yaml#/Include-channels
Expand All @@ -25,14 +29,24 @@ get:
$ref: ../../components/responses.yaml#/all-docs
'400':
$ref: ../../components/responses.yaml#/request-problem
'403':
description: This API endpoint has been disabled by the administrator.
content:
application/json:
schema:
$ref: ../../components/schemas.yaml#/HTTP-Error
'404':
$ref: ../../components/responses.yaml#/Not-found
tags:
- Document
operationId: get_keyspace-_all_docs
post:
summary: Get all the documents in the database using a built-in view
description: Returns all documents in the database based on the specified parameters.
description: |-
Returns all documents in the database based on the specified parameters.

This endpoint is not recommended for larger datasets or production workloads.
[GET /{keyspace}/_changes](#operation/get_keyspace-_changes) or [POST /{keyspace}/_bulk_get](#operation/post_keyspace-_bulk_get) have more efficient implementations and should be used instead.
parameters:
- $ref: ../../components/parameters.yaml#/include_docs
- $ref: ../../components/parameters.yaml#/Include-channels
Expand Down Expand Up @@ -60,6 +74,12 @@ post:
$ref: ../../components/responses.yaml#/all-docs
'400':
$ref: ../../components/responses.yaml#/request-problem
'403':
description: This API endpoint has been disabled by the administrator.
content:
application/json:
schema:
$ref: ../../components/schemas.yaml#/HTTP-Error
'404':
$ref: ../../components/responses.yaml#/Not-found
tags:
Expand Down
38 changes: 3 additions & 35 deletions rest/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,7 @@ func TestStarAccess(t *testing.T) {

base.SetUpTestLogging(t, base.LevelDebug, base.KeyChanges)

type allDocsRow struct {
ID string `json:"id"`
Key string `json:"key"`
Value struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
} `json:"value"`
Doc db.Body `json:"doc,omitempty"`
Error string `json:"error"`
}
var allDocsResult struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Rows []allDocsRow `json:"rows"`
}
var allDocsResult allDocsResponse

// Create some docs:
rt := NewRestTester(t, &RestTesterConfig{SyncFn: channels.DocChannelsSyncFunction})
Expand Down Expand Up @@ -551,23 +536,6 @@ func TestAllDocsAccessControl(t *testing.T) {
rt := NewRestTester(t, &RestTesterConfig{SyncFn: channels.DocChannelsSyncFunction})
defer rt.Close()

type allDocsRow struct {
ID string `json:"id"`
Key string `json:"key"`
Value struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
} `json:"value"`
Doc db.Body `json:"doc,omitempty"`
Error string `json:"error"`
}
type allDocsResponse struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Rows []allDocsRow `json:"rows"`
}

// Create some docs:
a := auth.NewAuthenticator(rt.MetadataStore(), nil, rt.GetDatabase().AuthenticatorOptions(rt.Context()))
a.Collections = rt.GetDatabase().CollectionNames
Expand Down Expand Up @@ -707,13 +675,13 @@ func TestAllDocsAccessControl(t *testing.T) {
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[0].Value.Channels)
assert.Equal(t, "doc1", allDocsResult.Rows[1].Key)
assert.Equal(t, "forbidden", allDocsResult.Rows[1].Error)
assert.Equal(t, "", allDocsResult.Rows[1].Value.Rev)
assert.Nil(t, allDocsResult.Rows[1].Value)
assert.Equal(t, "doc3", allDocsResult.Rows[2].ID)
assert.Equal(t, []string{"Cinemax"}, allDocsResult.Rows[2].Value.Channels)
assert.Equal(t, "1-20912648f85f2bbabefb0993ddd37b41", allDocsResult.Rows[2].Value.Rev)
assert.Equal(t, "b0gus", allDocsResult.Rows[3].Key)
assert.Equal(t, "not_found", allDocsResult.Rows[3].Error)
assert.Equal(t, "", allDocsResult.Rows[3].Value.Rev)
assert.Nil(t, allDocsResult.Rows[3].Value)

// Check GET to _all_docs with keys parameter:
response = rt.SendUserRequest(http.MethodGet, "/{{.keyspace}}/_all_docs?channels=true&keys=%5B%22doc4%22%2C%22doc1%22%2C%22doc3%22%2C%22b0gus%22%5D", "", "alice")
Expand Down
17 changes: 1 addition & 16 deletions rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,22 +1228,7 @@ func TestResponseEncoding(t *testing.T) {

func TestAllDocsChannelsAfterChannelMove(t *testing.T) {

type allDocsRow struct {
ID string `json:"id"`
Key string `json:"key"`
Value struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
} `json:"value"`
Doc db.Body `json:"doc,omitempty"`
Error string `json:"error"`
}
var allDocsResult struct {
TotalRows int `json:"total_rows"`
Offset int `json:"offset"`
Rows []allDocsRow `json:"rows"`
}
var allDocsResult allDocsResponse

rtConfig := RestTesterConfig{SyncFn: `function(doc) {channel(doc.channels)}`}
rt := NewRestTester(t, &rtConfig)
Expand Down
40 changes: 25 additions & 15 deletions rest/bulk_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,33 @@ import (
"github.com/couchbase/sync_gateway/db"
)

type allDocsResponse struct {
Rows []allDocsRow `json:"rows"`
TotalRows int `json:"total_rows"`
UpdateSeq uint64 `json:"update_seq"`
}

type allDocsRowValue struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
}
type allDocsRow struct {
Key string `json:"key"`
ID string `json:"id,omitempty"`
Value *allDocsRowValue `json:"value,omitempty"`
Doc json.RawMessage `json:"doc,omitempty"`
UpdateSeq uint64 `json:"update_seq,omitempty"`
Error string `json:"error,omitempty"`
Status int `json:"status,omitempty"`
}

// HTTP handler for _all_docs
func (h *handler) handleAllDocs() error {
if h.privs != adminPrivs && h.db.DatabaseContext.Options.DisablePublicAllDocs {
return base.HTTPErrorf(http.StatusForbidden, "public access to _all_docs is disabled for this database")
}

// http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API
includeDocs := h.getBoolQuery("include_docs")
includeChannels := h.getBoolQuery("channels")
Expand Down Expand Up @@ -99,21 +124,6 @@ func (h *handler) handleAllDocs() error {
return result
}

type allDocsRowValue struct {
Rev string `json:"rev"`
Channels []string `json:"channels,omitempty"`
Access map[string]base.Set `json:"access,omitempty"` // for admins only
}
type allDocsRow struct {
Key string `json:"key"`
ID string `json:"id,omitempty"`
Value *allDocsRowValue `json:"value,omitempty"`
Doc json.RawMessage `json:"doc,omitempty"`
UpdateSeq uint64 `json:"update_seq,omitempty"`
Error string `json:"error,omitempty"`
Status int `json:"status,omitempty"`
}

// Subroutine that creates a response row for a document:
totalRows := 0
createRow := func(doc db.IDRevAndSequence, channels []string) *allDocsRow {
Expand Down
81 changes: 81 additions & 0 deletions rest/bulk_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2025-Present Couchbase, Inc.
//
// Use of this software is governed by the Business Source License included
// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
// in that file, in accordance with the Business Source License, use of this
// software will be governed by the Apache License, Version 2.0, included in
// the file licenses/APL2.txt.

package rest

import (
"encoding/json"
"net/http"
"testing"

"github.com/couchbase/sync_gateway/base"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDisablePublicAllDocs(t *testing.T) {
tests := []struct {
name string
disablePublicAllDocs *bool
expectedPublicStatus int
expectedPublicError string
}{
{
name: "default",
disablePublicAllDocs: nil,
expectedPublicStatus: http.StatusOK,
},
{
name: "disabled",
disablePublicAllDocs: base.Ptr(true),
expectedPublicStatus: http.StatusForbidden,
expectedPublicError: "public access to _all_docs is disabled for this database",
},
{
name: "enabled",
disablePublicAllDocs: base.Ptr(false),
expectedPublicStatus: http.StatusOK,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rtConfig := RestTesterConfig{
DatabaseConfig: &DatabaseConfig{
DbConfig: DbConfig{
DisablePublicAllDocs: test.disablePublicAllDocs,
},
},
}
rt := NewRestTester(t, &rtConfig)
defer rt.Close()

rt.CreateUser("user1", nil)
rt.CreateTestDoc("doc1")
rt.CreateTestDoc("doc2")

t.Run("public", func(t *testing.T) {
response := rt.SendUserRequest("GET", "/{{.keyspace}}/_all_docs", "", "user1")
RequireStatus(t, response, test.expectedPublicStatus)
if test.expectedPublicError != "" {
require.Contains(t, response.Body.String(), test.expectedPublicError)
}
})

t.Run("admin", func(t *testing.T) {
response := rt.SendAdminRequest("GET", "/{{.keyspace}}/_all_docs", "")
RequireStatus(t, response, http.StatusOK)
var result allDocsResponse
require.NoError(t, json.Unmarshal(response.Body.Bytes(), &result))
assert.Equal(t, 2, len(result.Rows))
assert.Equal(t, "doc1", result.Rows[0].ID)
assert.Equal(t, "doc2", result.Rows[1].ID)
})
})
}
}
1 change: 1 addition & 0 deletions rest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ type DbConfig struct {
Logging *DbLoggingConfig `json:"logging,omitempty"` // Per-database Logging config
UpdatedAt *time.Time `json:"updated_at,omitempty"` // Time at which the database config was last updated
CreatedAt *time.Time `json:"created_at,omitempty"` // Time at which the database config was created
DisablePublicAllDocs *bool `json:"disable_public_all_docs,omitempty"` // Whether to disable public access to the _all_docs endpoint for this database
}

type ScopesConfig map[string]ScopeConfig
Expand Down
1 change: 1 addition & 0 deletions rest/config_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func DefaultDbConfig(sc *StartupConfig, useXattrs bool) *DbConfig {
JavascriptTimeoutSecs: base.Ptr(base.DefaultJavascriptTimeoutSecs),
Suspendable: base.Ptr(sc.IsServerless()),
Logging: DefaultPerDBLogging(sc.Logging),
DisablePublicAllDocs: base.Ptr(false),
}

if useXattrs {
Expand Down
8 changes: 7 additions & 1 deletion rest/server_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -1349,14 +1349,19 @@ func dbcOptionsFromConfig(ctx context.Context, sc *ServerContext, config *DbConf
}

if config.Unsupported.DisableCleanSkippedQuery {
base.WarnfCtx(ctx, `Deprecation notice: setting databse configuration option "disable_clean_skipped_query" no longer has any functionality. In the future, this option will be removed.`)
base.WarnfCtx(ctx, `Deprecation notice: setting database configuration option "disable_clean_skipped_query" no longer has any functionality. In the future, this option will be removed.`)
}
// If basic auth is disabled, it doesn't make sense to send WWW-Authenticate
sendWWWAuthenticate := config.SendWWWAuthenticateHeader
if base.ValDefault(config.DisablePasswordAuth, false) {
sendWWWAuthenticate = base.Ptr(false)
}

disablePublicAllDocs := base.ValDefault(config.DisablePublicAllDocs, false)
if !disablePublicAllDocs {
base.WarnfCtx(ctx, `Deprecation notice: setting database configuration option "disable_public_all_docs" to false is deprecated. In the future, public access to the all_docs API will be disabled by default.`)
}

contextOptions := db.DatabaseContextOptions{
CacheOptions: &cacheOptions,
RevisionCacheOptions: revCacheOptions,
Expand Down Expand Up @@ -1392,6 +1397,7 @@ func dbcOptionsFromConfig(ctx context.Context, sc *ServerContext, config *DbConf
MaxConcurrentChangesBatches: sc.Config.Replicator.MaxConcurrentChangesBatches,
MaxConcurrentRevs: sc.Config.Replicator.MaxConcurrentRevs,
NumIndexReplicas: config.numIndexReplicas(),
DisablePublicAllDocs: disablePublicAllDocs,
}

if config.Index != nil && config.Index.NumPartitions != nil {
Expand Down
Loading