Skip to content

Commit e5e63fe

Browse files
committed
Merge remote-tracking branch 'origin/main' into CBG-5198
2 parents 8468ff8 + 825d495 commit e5e63fe

70 files changed

Lines changed: 3993 additions & 859 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

auth/auth.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,17 @@ func (auth *Authenticator) Save(p Principal) error {
428428
return nil
429429
}
430430

431-
// Used for resync
432-
func (auth *Authenticator) UpdateSequenceNumber(p Principal, seq uint64) error {
431+
// Used for resync. Cancels update if provided resyncID matches existing on the principal
432+
func (auth *Authenticator) UpdateSequenceNumberForResync(p Principal, seq uint64, resyncID string) error {
433+
434+
if resyncID == "" {
435+
return errors.New("resyncID must not be empty")
436+
}
437+
if p.ResyncID() == resyncID {
438+
return nil
439+
}
433440
p.SetSequence(seq)
441+
p.SetResyncID(resyncID)
434442
casOut, writeErr := auth.datastore.WriteCas(auth.LogCtx, p.DocID(), 0, p.Cas(), p, 0)
435443
if writeErr != nil {
436444
return writeErr

auth/auth_test.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2959,6 +2959,228 @@ func TestInvalidateChannels(t *testing.T) {
29592959
}
29602960
}
29612961

2962+
// getRawPrincipal fetches the principal doc directly from the datastore, avoiding the channel
2963+
// rebuild performed by GetUser/GetRole, so invalidation sequences can be inspected.
2964+
func getRawPrincipal(t *testing.T, auth *Authenticator, name string, isUser bool) Principal {
2965+
ctx := base.TestCtx(t)
2966+
var princ Principal
2967+
var docID string
2968+
if isUser {
2969+
docID = auth.DocIDForUser(name)
2970+
princ = &userImpl{roleImpl: roleImpl{docID: docID}}
2971+
} else {
2972+
docID = auth.DocIDForRole(name)
2973+
princ = &roleImpl{docID: docID}
2974+
}
2975+
cas, err := auth.datastore.Get(ctx, docID, &princ)
2976+
require.NoError(t, err)
2977+
princ.SetCas(cas)
2978+
return princ
2979+
}
2980+
2981+
// createTestPrincipal creates and saves a user or role with a single explicit channel grant.
2982+
func createTestPrincipal(t *testing.T, auth *Authenticator, name string, isUser bool) {
2983+
var princ Principal
2984+
var err error
2985+
if isUser {
2986+
princ, err = auth.NewUser(name, "password", ch.BaseSetOf(t, "ABC"))
2987+
} else {
2988+
princ, err = auth.NewRole(name, ch.BaseSetOf(t, "ABC"))
2989+
}
2990+
require.NoError(t, err)
2991+
require.NoError(t, auth.Save(princ))
2992+
}
2993+
2994+
// TestInvalidateChannelsIdempotent verifies that re-invalidating principals at the same or a
2995+
// later sequence preserves the original invalidation sequence and does not write to the store.
2996+
// It covers InvalidateDefaultChannels, InvalidateChannels, InvalidateRolesAndChannels, and
2997+
// UpdateSequenceNumberForResync under a single test bucket.
2998+
func TestInvalidateChannelsIdempotent(t *testing.T) {
2999+
const (
3000+
scopeName = "scope1"
3001+
collectionName = "collection1"
3002+
)
3003+
3004+
ctx := base.TestCtx(t)
3005+
bucket := base.GetTestBucket(t)
3006+
defer bucket.Close(ctx)
3007+
3008+
collections := base.ScopeAndCollectionNames{
3009+
base.DefaultScopeAndCollectionName(),
3010+
base.NewScopeAndCollectionName(scopeName, collectionName),
3011+
}
3012+
3013+
t.Run("InvalidateDefaultChannels", func(t *testing.T) {
3014+
auth := NewTestAuthenticator(t, bucket.GetSingleDataStore(), nil, DefaultAuthenticatorOptions(ctx))
3015+
3016+
testCases := []struct {
3017+
name string
3018+
principalName string
3019+
isUser bool
3020+
}{
3021+
{name: "User", principalName: "idempotent_default_user", isUser: true},
3022+
{name: "Role", principalName: "idempotent_default_role", isUser: false},
3023+
}
3024+
for _, testCase := range testCases {
3025+
t.Run(testCase.name, func(t *testing.T) {
3026+
createTestPrincipal(t, auth, testCase.principalName, testCase.isUser)
3027+
3028+
require.NoError(t, auth.InvalidateDefaultChannels(testCase.principalName, testCase.isUser, 5))
3029+
princ := getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3030+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3031+
casAfterFirst := princ.Cas()
3032+
3033+
// Re-invalidating at the same sequence must not write (CAS unchanged)
3034+
require.NoError(t, auth.InvalidateDefaultChannels(testCase.principalName, testCase.isUser, 5))
3035+
princ = getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3036+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3037+
require.Equal(t, casAfterFirst, princ.Cas())
3038+
3039+
// Re-invalidating at a later sequence must keep the original invalidation sequence and not write
3040+
require.NoError(t, auth.InvalidateDefaultChannels(testCase.principalName, testCase.isUser, 20))
3041+
princ = getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3042+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3043+
require.Equal(t, casAfterFirst, princ.Cas())
3044+
})
3045+
}
3046+
})
3047+
3048+
t.Run("InvalidateChannels", func(t *testing.T) {
3049+
options := DefaultAuthenticatorOptions(ctx)
3050+
options.Collections = map[string]map[string]struct{}{
3051+
base.DefaultScope: {base.DefaultCollection: struct{}{}},
3052+
scopeName: {collectionName: struct{}{}},
3053+
}
3054+
auth := NewTestAuthenticator(t, bucket.GetSingleDataStore(), &mockComputer{}, options)
3055+
3056+
testCases := []struct {
3057+
name string
3058+
principalName string
3059+
isUser bool
3060+
}{
3061+
{name: "User", principalName: "idempotent_channels_user", isUser: true},
3062+
{name: "Role", principalName: "idempotent_channels_role", isUser: false},
3063+
}
3064+
for _, testCase := range testCases {
3065+
t.Run(testCase.name, func(t *testing.T) {
3066+
createTestPrincipal(t, auth, testCase.principalName, testCase.isUser)
3067+
3068+
// Invalidating more than one collection forces the update (non-subdoc) pathway
3069+
require.NoError(t, auth.InvalidateChannels(testCase.principalName, testCase.isUser, collections, 5))
3070+
princ := getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3071+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3072+
require.Equal(t, uint64(5), princ.getCollectionChannelInvalSeq(scopeName, collectionName))
3073+
casAfterFirst := princ.Cas()
3074+
3075+
// Re-invalidating at the same sequence must not write (CAS unchanged)
3076+
require.NoError(t, auth.InvalidateChannels(testCase.principalName, testCase.isUser, collections, 5))
3077+
princ = getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3078+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3079+
require.Equal(t, uint64(5), princ.getCollectionChannelInvalSeq(scopeName, collectionName))
3080+
require.Equal(t, casAfterFirst, princ.Cas())
3081+
3082+
// Re-invalidating at a later sequence must keep the original invalidation sequence and not write
3083+
require.NoError(t, auth.InvalidateChannels(testCase.principalName, testCase.isUser, collections, 20))
3084+
princ = getRawPrincipal(t, auth, testCase.principalName, testCase.isUser)
3085+
require.Equal(t, uint64(5), princ.GetChannelInvalSeq())
3086+
require.Equal(t, uint64(5), princ.getCollectionChannelInvalSeq(scopeName, collectionName))
3087+
require.Equal(t, casAfterFirst, princ.Cas())
3088+
})
3089+
}
3090+
})
3091+
3092+
t.Run("InvalidateRolesAndChannels", func(t *testing.T) {
3093+
computer := mockComputer{roles: ch.AtSequence(ch.BaseSetOf(t, "role1"), 1)}
3094+
options := DefaultAuthenticatorOptions(ctx)
3095+
options.Collections = map[string]map[string]struct{}{
3096+
base.DefaultScope: {base.DefaultCollection: struct{}{}},
3097+
scopeName: {collectionName: struct{}{}},
3098+
}
3099+
auth := NewTestAuthenticator(t, bucket.GetSingleDataStore(), &computer, options)
3100+
3101+
t.Run("First invalidation sets sequence", func(t *testing.T) {
3102+
createTestPrincipal(t, auth, "invalidate_roles_channels_first", true)
3103+
3104+
require.NoError(t, auth.InvalidateRolesAndChannels("invalidate_roles_channels_first", collections, 5))
3105+
user := getRawPrincipal(t, auth, "invalidate_roles_channels_first", true).(*userImpl)
3106+
require.Equal(t, uint64(5), user.GetRoleInvalSeq())
3107+
require.Equal(t, uint64(5), user.GetChannelInvalSeq())
3108+
require.Equal(t, uint64(5), user.getCollectionChannelInvalSeq(scopeName, collectionName))
3109+
})
3110+
3111+
t.Run("Re-invalidation at later sequence preserves original", func(t *testing.T) {
3112+
createTestPrincipal(t, auth, "invalidate_roles_channels_reinvalidate", true)
3113+
3114+
require.NoError(t, auth.InvalidateRolesAndChannels("invalidate_roles_channels_reinvalidate", collections, 5))
3115+
casAfterFirst := getRawPrincipal(t, auth, "invalidate_roles_channels_reinvalidate", true).Cas()
3116+
3117+
require.NoError(t, auth.InvalidateRolesAndChannels("invalidate_roles_channels_reinvalidate", collections, 20))
3118+
user := getRawPrincipal(t, auth, "invalidate_roles_channels_reinvalidate", true).(*userImpl)
3119+
require.Equal(t, uint64(5), user.GetRoleInvalSeq())
3120+
require.Equal(t, uint64(5), user.GetChannelInvalSeq())
3121+
require.Equal(t, uint64(5), user.getCollectionChannelInvalSeq(scopeName, collectionName))
3122+
require.Equal(t, casAfterFirst, user.Cas())
3123+
})
3124+
})
3125+
3126+
t.Run("UpdateSequenceNumberForResync", func(t *testing.T) {
3127+
auth := NewTestAuthenticator(t, bucket.GetSingleDataStore(), nil, DefaultAuthenticatorOptions(ctx))
3128+
3129+
t.Run("Empty resyncID error", func(t *testing.T) {
3130+
user, err := auth.NewUser("resync_user_empty", "pass", nil)
3131+
require.NoError(t, err)
3132+
require.NoError(t, auth.Save(user))
3133+
3134+
err = auth.UpdateSequenceNumberForResync(user, 10, "")
3135+
assert.Error(t, err)
3136+
assert.Contains(t, err.Error(), "resyncID must not be empty")
3137+
})
3138+
3139+
t.Run("Matching resyncID (CAS should NOT update)", func(t *testing.T) {
3140+
user, err := auth.NewUser("resync_user_match", "pass", nil)
3141+
require.NoError(t, err)
3142+
require.NoError(t, auth.Save(user))
3143+
3144+
// First call with "resync-1" - updates CAS
3145+
require.NoError(t, auth.UpdateSequenceNumberForResync(user, 10, "resync-1"))
3146+
3147+
dbUser, err := auth.GetUser("resync_user_match")
3148+
require.NoError(t, err)
3149+
initialCas := dbUser.Cas()
3150+
3151+
// Second call with "resync-1" - should be skipped, CAS must not update
3152+
require.NoError(t, auth.UpdateSequenceNumberForResync(dbUser, 20, "resync-1"))
3153+
3154+
finalUser, err := auth.GetUser("resync_user_match")
3155+
require.NoError(t, err)
3156+
assert.Equal(t, initialCas, finalUser.Cas(), "CAS should not have changed")
3157+
assert.Equal(t, uint64(10), finalUser.Sequence(), "Sequence should remain unchanged")
3158+
})
3159+
3160+
t.Run("Mismatching resyncID (CAS should update)", func(t *testing.T) {
3161+
user, err := auth.NewUser("resync_user_mismatch", "pass", nil)
3162+
require.NoError(t, err)
3163+
require.NoError(t, auth.Save(user))
3164+
3165+
// First call with "resync-1"
3166+
require.NoError(t, auth.UpdateSequenceNumberForResync(user, 10, "resync-1"))
3167+
3168+
dbUser, err := auth.GetUser("resync_user_mismatch")
3169+
require.NoError(t, err)
3170+
casAfterFirst := dbUser.Cas()
3171+
3172+
// Second call with "resync-2" - mismatch, should update CAS
3173+
require.NoError(t, auth.UpdateSequenceNumberForResync(dbUser, 20, "resync-2"))
3174+
3175+
finalUser, err := auth.GetUser("resync_user_mismatch")
3176+
require.NoError(t, err)
3177+
assert.NotEqual(t, casAfterFirst, finalUser.Cas(), "CAS must be updated")
3178+
assert.Equal(t, uint64(20), finalUser.Sequence(), "Sequence should be updated to 20")
3179+
assert.Equal(t, "resync-2", finalUser.ResyncID(), "ResyncID should be updated to resync-2")
3180+
})
3181+
})
3182+
}
3183+
29623184
func TestCalculateMaxHistoryEntriesPerGrant(t *testing.T) {
29633185
testCases := []struct {
29643186
Name string

auth/principal.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ type Principal interface {
5858
// Sets the created time for the principal document
5959
SetCreatedAt(t time.Time)
6060

61+
// ResyncID returns a ID used to update sequence for a resync operation. If regenerate_sequences=false, this will be
62+
// an empty string.
63+
ResyncID() string
64+
// SetResyncID sets the resync ID.
65+
SetResyncID(resyncID string)
66+
6167
CompactChannelHistory(scope string, col string, channels []string) []string
6268

6369
// Principal includes the PrincipalCollectionAccess interface for operations against

auth/role.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type roleImpl struct {
3535
CollectionsAccess map[string]map[string]*CollectionAccess `json:"collection_access,omitempty"` // Nested maps of CollectionAccess, indexed by scope and collection name
3636
UpdatedAt time.Time `json:"updated_at"`
3737
CreatedAt time.Time `json:"created_at"`
38+
ResyncID_ string `json:"resync_id,omitempty"` // Last known ID for a resync operation. Used to ensure updating principals on multiple nodes is OK.
3839
cas uint64
3940
docID string // key used to store the roleImpl
4041
}
@@ -364,6 +365,18 @@ func (role *roleImpl) ChannelHistory() TimedSetHistory {
364365
return role.ChannelHistory_
365366
}
366367

368+
// ResyncID returns the last UUID of a resync operation that updated the sequence of this principal object. Otherwise,
369+
// returns empty string.
370+
func (role *roleImpl) ResyncID() string {
371+
return role.ResyncID_
372+
}
373+
374+
// SetResyncID sets an ID for the resync operation that updates this principal. Used to restrict updates for this object
375+
// to once per resync operation.
376+
func (role *roleImpl) SetResyncID(resyncID string) {
377+
role.ResyncID_ = resyncID
378+
}
379+
367380
// Checks whether this role object contains valid data; if not, returns an error.
368381
func (role *roleImpl) validate() error {
369382
if !IsValidPrincipalName(role.Name_) {

0 commit comments

Comments
 (0)