Skip to content

Commit f9f7980

Browse files
committed
all unsupported db option for samesite
1 parent 5648c98 commit f9f7980

File tree

11 files changed

+203
-62
lines changed

11 files changed

+203
-62
lines changed

db/database.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ type DatabaseContext struct {
154154
MetadataKeys *base.MetadataKeys // Factory to generate metadata document keys
155155
RequireResync base.ScopeAndCollectionNames // Collections requiring resync before database can go online
156156
RequireAttachmentMigration base.ScopeAndCollectionNames // Collections that require the attachment migration background task to run against
157-
CORS *auth.CORSConfig // CORS configuration
158157
EnableMou bool // Write _mou xattr when performing metadata-only update. Set based on bucket capability on connect
159158
WasInitializedSynchronously bool // true if the database was initialized synchronously
160159
BroadcastSlowMode atomic.Bool // bool to indicate if a slower ticker value should be used to notify changes feeds of changes
@@ -164,6 +163,7 @@ type DatabaseContext struct {
164163
CachedCCVStartingCas *base.VBucketCAS // If set, the cached value of the CCV starting CAS value to avoid repeated lookups
165164
CachedCCVEnabled atomic.Bool // If set, the cached value of the CCV Enabled flag (this is not expected to transition from true->false, but could go false->true)
166165
numVBuckets uint16 // Number of vbuckets in the bucket
166+
sameSiteCookieMode http.SameSite
167167
}
168168

169169
type Scope struct {
@@ -216,6 +216,7 @@ type DatabaseContextOptions struct {
216216
ImportVersion uint64 // Version included in import DCP checkpoints, incremented when collections added to db
217217
DisablePublicAllDocs bool // Disable public access to the _all_docs endpoint for this database
218218
StoreLegacyRevTreeData *bool // Whether to store additional data for legacy rev tree support in delta sync and replication backup revs
219+
CORS auth.CORSConfig // An empty CORS configuration is considered to have no CORS enabled
219220
}
220221

221222
type ConfigPrincipals struct {
@@ -278,6 +279,7 @@ type UnsupportedOptions struct {
278279
KVBufferSize int `json:"kv_buffer,omitempty"` // Enables user to set their own KV pool buffer
279280
BlipSendDocsWithChannelRemoval bool `json:"blip_send_docs_with_channel_removal,omitempty"` // Enables sending docs with channel removals using channel filters
280281
RejectWritesWithSkippedSequences bool `json:"reject_writes_with_skipped_sequences,omitempty"` // Reject writes if there are skipped sequences in the database
282+
SameSiteCookie *string `json:"same_site_cookie,omitempty"` // Sets the SameSite attribute on session cookies.
281283
}
282284

283285
type WarningThresholds struct {
@@ -433,6 +435,7 @@ func NewDatabaseContext(ctx context.Context, dbName string, bucket base.Bucket,
433435
ServerUUID: serverUUID,
434436
UserFunctionTimeout: defaultUserFunctionTimeout,
435437
CachedCCVStartingCas: &base.VBucketCAS{},
438+
sameSiteCookieMode: http.SameSiteDefaultMode,
436439
}
437440
dbContext.numVBuckets, err = bucket.GetMaxVbno()
438441
if err != nil {
@@ -443,6 +446,17 @@ func NewDatabaseContext(ctx context.Context, dbName string, bucket base.Bucket,
443446
return nil, err
444447
}
445448

449+
if !dbContext.CORS().IsEmpty() {
450+
dbContext.sameSiteCookieMode = http.SameSiteNoneMode
451+
}
452+
if dbContext.Options.UnsupportedOptions != nil {
453+
var err error
454+
dbContext.sameSiteCookieMode, err = dbContext.Options.UnsupportedOptions.GetSameSiteCookieMode()
455+
if err != nil {
456+
return nil, err
457+
}
458+
}
459+
446460
// set up cancellable context based on the background context (context lifecycle for the database
447461
// must be distinct from the request context associated with the db create/update). Used to trigger
448462
// teardown of connected replications on database close.
@@ -2585,3 +2599,28 @@ func PurgeDCPCheckpoints(ctx context.Context, database *DatabaseContext, checkpo
25852599
func (db *DatabaseContext) EnableAllowConflicts(tb testing.TB) {
25862600
db.Options.AllowConflicts = base.Ptr(true)
25872601
}
2602+
2603+
// CORS returns the CORS configuration for the database.
2604+
func (db *DatabaseContext) CORS() *auth.CORSConfig {
2605+
return &db.Options.CORS
2606+
}
2607+
2608+
// GetSameSiteCookieMode returns the http.SameSite mode based on the unsupported database options. Returns an error if
2609+
// an invalid string is set.
2610+
func (o *UnsupportedOptions) GetSameSiteCookieMode() (http.SameSite, error) {
2611+
if o == nil || o.SameSiteCookie == nil {
2612+
return http.SameSiteDefaultMode, nil
2613+
}
2614+
switch *o.SameSiteCookie {
2615+
case "Lax":
2616+
return http.SameSiteLaxMode, nil
2617+
case "Strict":
2618+
return http.SameSiteStrictMode, nil
2619+
case "None":
2620+
return http.SameSiteNoneMode, nil
2621+
case "Default":
2622+
return http.SameSiteDefaultMode, nil
2623+
default:
2624+
return http.SameSiteDefaultMode, fmt.Errorf("unsupported_options.same_site_cookie option %q is not valid, choices are \"Lax\", \"Strict\", and \"None", *o.SameSiteCookie)
2625+
}
2626+
}

docs/api/components/schemas.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,15 @@ Database:
16981698
remote_config_tls_skip_verify:
16991699
description: Enable self-signed certificates for external JavaScript load.
17001700
type: boolean
1701+
same_site_cookie:
1702+
description: |-
1703+
Override the session cookie SameSite behavior. By default, if CORS is enabled, a session cookie will have SameSite:None. `Default` will omit any SameSite option from the cookie.
1704+
type: string
1705+
enum:
1706+
- "Default"
1707+
- "Lax"
1708+
- "None"
1709+
- "Strict"
17011710
sgr_tls_skip_verify:
17021711
description: Enable self-signed certificates for SG-replicate testing.
17031712
type: boolean

rest/api_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,15 @@ func TestCORSOrigin(t *testing.T) {
381381
sc := rt.ServerContext()
382382
defer func() {
383383
sc.Config.API.CORS.Origin = defaultTestingCORSOrigin
384+
rt.GetDatabase().Options.CORS.Origin = defaultTestingCORSOrigin
384385
}()
385386

386-
sc.Config.API.CORS.Origin = []string{"http://example.com", "http://staging.example.com"}
387+
updatedOrigin := []string{"http://example.com", "http://staging.example.com"}
388+
sc.Config.API.CORS.Origin = updatedOrigin
389+
rt.GetDatabase().Options.CORS.Origin = updatedOrigin
387390
if !base.StringSliceContains(sc.Config.API.CORS.Origin, tc.origin) {
388391
for _, method := range []string{http.MethodGet, http.MethodOptions} {
389-
response := rt.SendRequestWithHeaders(method, "/{{.keyspace}}/", "", reqHeaders)
392+
response := rt.SendRequestWithHeaders(method, "/{{.db}}/", "", reqHeaders)
390393
assert.Equal(t, "", response.Header().Get("Access-Control-Allow-Origin"))
391394
}
392395
}

rest/blip_sync.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (h *handler) handleBLIPSync() error {
4949
}
5050

5151
// error is checked at the time of database load, and ignored at this time
52-
originPatterns, _ := hostOnlyCORS(h.db.CORS.Origin)
52+
originPatterns, _ := hostOnlyCORS(h.db.CORS().Origin)
5353

5454
cancelCtx, cancelCtxFunc := context.WithCancel(h.db.DatabaseContext.CancelContext)
5555
// Create a BLIP context:

rest/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,13 @@ func (dbConfig *DbConfig) validateVersion(ctx context.Context, isEnterpriseEditi
10831083
}
10841084
}
10851085

1086+
if dbConfig.Unsupported != nil {
1087+
_, err := dbConfig.Unsupported.GetSameSiteCookieMode()
1088+
if err != nil {
1089+
multiError = multiError.Append(err)
1090+
}
1091+
}
1092+
10861093
if validateReplications {
10871094
for name, r := range dbConfig.Replications {
10881095
if name == "" {

rest/config_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3378,3 +3378,62 @@ func TestConfigUserXattrKeyValidation(t *testing.T) {
33783378
})
33793379
}
33803380
}
3381+
3382+
func TestValidateUnsupportedSameSiteCookies(t *testing.T) {
3383+
tests := []struct {
3384+
name string
3385+
unsupportedSettings *db.UnsupportedOptions
3386+
error string
3387+
}{
3388+
{
3389+
name: "no unsupportedSettings present",
3390+
unsupportedSettings: &db.UnsupportedOptions{},
3391+
error: "",
3392+
},
3393+
{
3394+
name: "no samesite, unsupportedSettings present",
3395+
unsupportedSettings: &db.UnsupportedOptions{},
3396+
error: "",
3397+
},
3398+
{
3399+
name: "valid value Lax",
3400+
unsupportedSettings: &db.UnsupportedOptions{SameSiteCookie: base.Ptr("Lax")},
3401+
error: "",
3402+
},
3403+
{
3404+
name: "valid value Strict",
3405+
unsupportedSettings: &db.UnsupportedOptions{SameSiteCookie: base.Ptr("Strict")},
3406+
error: "",
3407+
},
3408+
{
3409+
name: "valid value None",
3410+
unsupportedSettings: &db.UnsupportedOptions{SameSiteCookie: base.Ptr("None")},
3411+
error: "",
3412+
},
3413+
{
3414+
name: "invalid value",
3415+
unsupportedSettings: &db.UnsupportedOptions{SameSiteCookie: base.Ptr("invalid value")},
3416+
error: "unsupported_options.same_site_cookie option",
3417+
},
3418+
}
3419+
3420+
for _, test := range tests {
3421+
t.Run(test.name, func(t *testing.T) {
3422+
dbConfig := DbConfig{
3423+
Name: "db",
3424+
Unsupported: test.unsupportedSettings,
3425+
}
3426+
3427+
validateReplications := false
3428+
validateOIDC := false
3429+
ctx := base.TestCtx(t)
3430+
err := dbConfig.validate(ctx, validateOIDC, validateReplications)
3431+
if test.error != "" {
3432+
require.Error(t, err)
3433+
require.Contains(t, err.Error(), test.error)
3434+
} else {
3435+
require.NoError(t, err)
3436+
}
3437+
})
3438+
}
3439+
}

rest/cors_test.go

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/couchbase/sync_gateway/auth"
1919
"github.com/couchbase/sync_gateway/base"
20+
"github.com/couchbase/sync_gateway/db"
2021
"github.com/stretchr/testify/assert"
2122
"github.com/stretchr/testify/require"
2223
)
@@ -409,67 +410,90 @@ func TestCORSLoginOriginPerDatabase(t *testing.T) {
409410
// Override the default (example.com) CORS configuration in the DbConfig for /db:
410411
rt := NewRestTesterPersistentConfigNoDB(t)
411412
defer rt.Close()
412-
dbConfig := rt.NewDbConfig()
413-
dbConfig.CORS = &auth.CORSConfig{
414-
Origin: []string{"http://couchbase.com", "http://staging.couchbase.com"},
415-
LoginOrigin: []string{"http://couchbase.com"},
416-
Headers: []string{},
417-
}
418-
RequireStatus(t, rt.CreateDatabase("dbloginorigin", dbConfig), http.StatusCreated)
419-
420-
const username = "alice"
421-
rt.CreateUser(username, nil)
422-
423413
testCases := []struct {
424-
name string
425-
origin string
426-
responseCode int
427-
responseErrorBody string
414+
name string
415+
unsupportedOptions *db.UnsupportedOptions
428416
}{
429417
{
430-
name: "CORS login origin allowed couchbase",
431-
origin: "http://couchbase.com",
432-
responseCode: http.StatusOK,
418+
name: "No unsupported options",
419+
unsupportedOptions: nil,
433420
},
434421
{
435-
name: "CORS login origin not allowed staging",
436-
origin: "http://staging.couchbase.com",
437-
responseCode: http.StatusBadRequest,
438-
responseErrorBody: "No CORS",
422+
name: "With unsupported options",
423+
unsupportedOptions: &db.UnsupportedOptions{
424+
SameSiteCookie: base.Ptr("Strict"),
425+
},
439426
},
440427
}
441428
for _, test := range testCases {
442429
rt.Run(test.name, func(t *testing.T) {
443-
reqHeaders := map[string]string{
444-
"Origin": test.origin,
445-
"Authorization": GetBasicAuthHeader(t, username, RestTesterDefaultUserPassword),
430+
// Override the default (example.com) CORS configuration in the DbConfig for /db:
431+
rt := NewRestTesterPersistentConfigNoDB(t)
432+
defer rt.Close()
433+
434+
dbConfig := rt.NewDbConfig()
435+
dbConfig.Unsupported = test.unsupportedOptions
436+
dbConfig.CORS = &auth.CORSConfig{
437+
Origin: []string{"http://couchbase.com", "http://staging.couchbase.com"},
438+
LoginOrigin: []string{"http://couchbase.com"},
439+
Headers: []string{},
446440
}
447-
resp := rt.SendRequestWithHeaders(http.MethodPost, "/{{.db}}/_session", "", reqHeaders)
448-
RequireStatus(t, resp, test.responseCode)
449-
if test.responseErrorBody != "" {
450-
require.Contains(t, resp.Body.String(), test.responseErrorBody)
451-
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
452-
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
453-
} else {
454-
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
455-
}
456-
if test.responseCode == http.StatusOK {
457-
cookie, err := http.ParseSetCookie(resp.Header().Get("Set-Cookie"))
458-
require.NoError(t, err)
459-
require.NotEmpty(t, cookie.Path)
460-
require.Equal(t, http.SameSiteNoneMode, cookie.SameSite)
461-
reqHeaders["Cookie"] = fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
462-
}
463-
resp = rt.SendRequestWithHeaders(http.MethodDelete, "/{{.db}}/_session", "", reqHeaders)
464-
RequireStatus(t, resp, test.responseCode)
465-
if test.responseErrorBody != "" {
466-
require.Contains(t, resp.Body.String(), test.responseErrorBody)
467-
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
468-
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
469-
} else {
470-
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
441+
RequireStatus(t, rt.CreateDatabase(SafeDatabaseName(t, test.name), dbConfig), http.StatusCreated)
442+
const username = "alice"
443+
rt.CreateUser(username, nil)
444+
445+
testCases := []struct {
446+
name string
447+
origin string
448+
responseCode int
449+
responseErrorBody string
450+
}{
451+
{
452+
name: "CORS login origin allowed couchbase",
453+
origin: "http://couchbase.com",
454+
responseCode: http.StatusOK,
455+
},
456+
{
457+
name: "CORS login origin not allowed staging",
458+
origin: "http://staging.couchbase.com",
459+
responseCode: http.StatusBadRequest,
460+
responseErrorBody: "No CORS",
461+
},
471462
}
463+
for _, test := range testCases {
464+
rt.Run(test.name, func(t *testing.T) {
465+
reqHeaders := map[string]string{
466+
"Origin": test.origin,
467+
"Authorization": GetBasicAuthHeader(t, username, RestTesterDefaultUserPassword),
468+
}
469+
resp := rt.SendRequestWithHeaders(http.MethodPost, "/{{.db}}/_session", "", reqHeaders)
470+
RequireStatus(t, resp, test.responseCode)
471+
if test.responseErrorBody != "" {
472+
require.Contains(t, resp.Body.String(), test.responseErrorBody)
473+
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
474+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
475+
} else {
476+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
477+
}
478+
if test.responseCode == http.StatusOK {
479+
cookie, err := http.ParseSetCookie(resp.Header().Get("Set-Cookie"))
480+
require.NoError(t, err)
481+
require.NotEmpty(t, cookie.Path)
482+
require.Equal(t, http.SameSiteNoneMode, cookie.SameSite)
483+
reqHeaders["Cookie"] = fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
484+
}
485+
resp = rt.SendRequestWithHeaders(http.MethodDelete, "/{{.db}}/_session", "", reqHeaders)
486+
RequireStatus(t, resp, test.responseCode)
487+
if test.responseErrorBody != "" {
488+
require.Contains(t, resp.Body.String(), test.responseErrorBody)
489+
// the access control headers are returned based on Origin and not LoginOrigin which could be considered a bug
490+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
491+
} else {
492+
require.Equal(t, test.origin, resp.Header().Get(accessControlAllowOrigin))
493+
}
472494

495+
})
496+
}
473497
})
474498
}
475499
}

rest/handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1785,7 +1785,7 @@ func (h *handler) pathTemplateContains(pattern string) bool {
17851785
// getCORSConfig will return the CORS config for the handler's database if set, otherwise it will return the server CORS config
17861786
func (h *handler) getCORSConfig() *auth.CORSConfig {
17871787
if h.db != nil {
1788-
return h.db.CORS
1788+
return h.db.CORS()
17891789
}
17901790
return h.server.Config.API.CORS
17911791
}

rest/routing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ func wrapRouter(sc *ServerContext, privs handlerPrivs, serverType serverType, ro
453453
if dbName != "" {
454454
db, err := h.server.GetActiveDatabase(dbName)
455455
if err == nil {
456-
cors = db.CORS
456+
cors = db.CORS()
457457
}
458458
}
459459
if !cors.IsEmpty() && privs != adminPrivs && privs != metricsPrivs {

rest/server_context.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -942,12 +942,6 @@ func (sc *ServerContext) _getOrAddDatabaseFromConfig(ctx context.Context, config
942942
dbcontext.RequireResync = collectionsRequiringResync
943943
dbcontext.RequireAttachmentMigration = collectionsRequiringAttachmentMigration
944944

945-
if config.CORS != nil {
946-
dbcontext.CORS = config.DbConfig.CORS
947-
} else {
948-
dbcontext.CORS = sc.Config.API.CORS
949-
}
950-
951945
if config.RevsLimit != nil {
952946
dbcontext.RevsLimit = *config.RevsLimit
953947
if dbcontext.AllowConflicts() {
@@ -1442,6 +1436,12 @@ func dbcOptionsFromConfig(ctx context.Context, sc *ServerContext, config *DbConf
14421436
StoreLegacyRevTreeData: base.Ptr(base.ValDefault(config.StoreLegacyRevTreeData, db.DefaultStoreLegacyRevTreeData)),
14431437
}
14441438

1439+
if config.CORS != nil {
1440+
contextOptions.CORS = *config.CORS
1441+
} else if sc.Config.API.CORS != nil {
1442+
contextOptions.CORS = *sc.Config.API.CORS
1443+
}
1444+
14451445
if config.Index != nil && config.Index.NumPartitions != nil {
14461446
contextOptions.NumIndexPartitions = config.Index.NumPartitions
14471447
}

0 commit comments

Comments
 (0)