Skip to content

Commit fdab8b5

Browse files
committed
feat: repair multiple permissions docs (fixes)
1 parent e170e2d commit fdab8b5

5 files changed

Lines changed: 90 additions & 76 deletions

File tree

model/permission/permissions.go

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import (
77
"encoding/json"
88
"fmt"
99
"net/http"
10+
"slices"
1011
"strings"
1112
"time"
1213

1314
build "github.com/cozy/cozy-stack/pkg/config"
14-
stackconfig "github.com/cozy/cozy-stack/pkg/config/config"
15+
"github.com/cozy/cozy-stack/pkg/config/config"
1516
"github.com/cozy/cozy-stack/pkg/consts"
1617
"github.com/cozy/cozy-stack/pkg/couchdb"
1718
"github.com/cozy/cozy-stack/pkg/couchdb/mango"
@@ -250,12 +251,6 @@ func GetForSharePreview(db prefixer.Prefixer, sharingID string) (*Permission, er
250251
// docs as part of the read by creating/updating the canonical permission doc
251252
// and deleting duplicate legacy docs.
252253
func GetForShareInteract(db prefixer.Prefixer, sharingID string) (*Permission, error) {
253-
mu := stackconfig.Lock().ReadWrite(db, shareInteractLockName(sharingID))
254-
if err := mu.Lock(); err != nil {
255-
return nil, err
256-
}
257-
defer mu.Unlock()
258-
259254
return getOrRepairShareInteractPermissions(db, sharingID)
260255
}
261256

@@ -294,30 +289,41 @@ func getShareInteractPermissions(db prefixer.Prefixer, sharingID string) ([]Perm
294289
}
295290

296291
func getOrRepairShareInteractPermissions(db prefixer.Prefixer, sharingID string) (*Permission, error) {
297-
return utils.RetryWithBackoffValue(context.Background(), shareInteractRetryOptions(), func() (*Permission, error) {
298-
perms, err := getShareInteractPermissions(db, sharingID)
299-
if err != nil {
300-
return nil, err
301-
}
302-
if canonical, needsRepair, err := shareInteractRepairState(perms, sharingID); err != nil || !needsRepair {
303-
return canonical, err
304-
}
292+
perms, err := getShareInteractPermissions(db, sharingID)
293+
if err != nil {
294+
return nil, err
295+
}
296+
if canonical, needsRepair, err := shareInteractRepairState(perms, sharingID); err != nil || !needsRepair {
297+
return canonical, err
298+
}
305299

306-
perm, retry, err := repairShareInteractPermissions(db, sharingID, perms)
307-
if err == nil && !retry {
308-
return perm, nil
309-
}
310-
if retry && err == nil {
311-
return nil, &couchdb.Error{
312-
StatusCode: http.StatusConflict,
313-
Name: "conflict",
314-
Reason: "could not repair share-interact permissions after retries",
315-
}
316-
}
300+
return repairShareInteractPermissionsWithLock(db, sharingID)
301+
}
302+
303+
func repairShareInteractPermissionsWithLock(db prefixer.Prefixer, sharingID string) (*Permission, error) {
304+
mu := config.Lock().ReadWrite(db, shareInteractLockName(sharingID))
305+
if err := mu.Lock(); err != nil {
317306
return nil, err
307+
}
308+
defer mu.Unlock()
309+
310+
return utils.RetryWithBackoffValue(context.Background(), shareInteractRetryOptions(), func() (*Permission, error) {
311+
return getOrRepairShareInteractPermissionsOnce(db, sharingID)
318312
})
319313
}
320314

315+
func getOrRepairShareInteractPermissionsOnce(db prefixer.Prefixer, sharingID string) (*Permission, error) {
316+
perms, err := getShareInteractPermissions(db, sharingID)
317+
if err != nil {
318+
return nil, err
319+
}
320+
if canonical, needsRepair, err := shareInteractRepairState(perms, sharingID); err != nil || !needsRepair {
321+
return canonical, err
322+
}
323+
324+
return repairShareInteractPermissions(db, sharingID, perms)
325+
}
326+
321327
func shareInteractRetryOptions() utils.RetryOptions {
322328
return utils.RetryOptions{
323329
Attempts: 5,
@@ -344,15 +350,17 @@ func shareInteractRepairState(perms []Permission, sharingID string) (*Permission
344350
usable := 0
345351
hasDuplicate := false
346352
for i := range perms {
347-
if perms[i].ID() != canonicalID {
353+
p := &perms[i]
354+
isCanonical := p.ID() == canonicalID
355+
if !isCanonical {
348356
hasDuplicate = true
349357
}
350-
if perms[i].Expired() {
358+
if p.Expired() {
351359
continue
352360
}
353361
usable++
354-
if perms[i].ID() == canonicalID {
355-
canonical = &perms[i]
362+
if isCanonical {
363+
canonical = p
356364
}
357365
}
358366
if usable == 0 {
@@ -364,7 +372,7 @@ func shareInteractRepairState(perms []Permission, sharingID string) (*Permission
364372
return nil, true, nil
365373
}
366374

367-
func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string, perms []Permission) (*Permission, bool, error) {
375+
func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string, perms []Permission) (*Permission, error) {
368376
canonicalID := ShareInteractPermissionID(sharingID)
369377
merged := &Permission{
370378
PID: canonicalID,
@@ -391,7 +399,7 @@ func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string, perm
391399
hasUsablePermission = true
392400

393401
if len(merged.Permissions) == 0 && len(p.Permissions) > 0 {
394-
merged.Permissions = p.Clone().(*Permission).Permissions
402+
merged.Permissions = slices.Clone(p.Permissions)
395403
}
396404
if merged.Metadata == nil && p.Metadata != nil {
397405
merged.Metadata = p.Metadata.Clone()
@@ -413,36 +421,27 @@ func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string, perm
413421
}
414422
}
415423
if !hasUsablePermission {
416-
return nil, false, ErrExpiredToken
424+
return nil, ErrExpiredToken
417425
}
418426

419427
if !hasCanonical {
420428
if err := couchdb.CreateNamedDoc(db, merged); err != nil {
421-
if couchdb.IsConflictError(err) || couchdb.IsFileExists(err) {
422-
return nil, true, err
423-
}
424-
return nil, false, err
429+
return nil, err
425430
}
426431
} else if err := couchdb.UpdateDoc(db, merged); err != nil {
427-
if couchdb.IsConflictError(err) {
428-
return nil, true, err
429-
}
430-
return nil, false, err
432+
return nil, err
431433
}
432434

433435
for _, p := range duplicates {
434436
if err := couchdb.DeleteDoc(db, p); err != nil {
435437
if couchdb.IsNotFoundError(err) {
436438
continue
437439
}
438-
if couchdb.IsConflictError(err) {
439-
return nil, true, err
440-
}
441-
return nil, false, err
440+
return nil, err
442441
}
443442
}
444443

445-
return merged, false, nil
444+
return merged, nil
446445
}
447446

448447
func getFromSource(db prefixer.Prefixer, permType, docType, slug string) (*Permission, error) {
@@ -820,14 +819,14 @@ func CreateShareInteractSet(db prefixer.Prefixer, sharingID string, codes map[st
820819
doc.Codes = make(map[string]string)
821820
}
822821

823-
mu := stackconfig.Lock().ReadWrite(db, shareInteractLockName(sharingID))
822+
mu := config.Lock().ReadWrite(db, shareInteractLockName(sharingID))
824823
if err := mu.Lock(); err != nil {
825824
return nil, err
826825
}
827826
defer mu.Unlock()
828827

829828
return utils.RetryWithBackoffValue(context.Background(), shareInteractRetryOptions(), func() (*Permission, error) {
830-
existing, err := getOrRepairShareInteractPermissions(db, sharingID)
829+
existing, err := getOrRepairShareInteractPermissionsOnce(db, sharingID)
831830
if err != nil && !couchdb.IsNotFoundError(err) {
832831
return nil, err
833832
}

model/permission/permissions_test.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import (
1717
"github.com/stretchr/testify/require"
1818
)
1919

20-
var testPrefix = prefixer.NewPrefixer(0, "test", "permission-tests")
20+
func testPrefix(t *testing.T) prefixer.Prefixer {
21+
t.Helper()
22+
return prefixer.NewPrefixer(0, "test", t.Name())
23+
}
2124

2225
func TestCheckDoctypeName(t *testing.T) {
2326
assert.NoError(t, CheckDoctypeName("io.cozy.files", false))
@@ -438,7 +441,8 @@ func TestGetForShareInteractRepairsDuplicateDocs(t *testing.T) {
438441
}
439442

440443
config.UseTestFile(t)
441-
require.NoError(t, couchdb.ResetDB(testPrefix, consts.Permissions))
444+
db := testPrefix(t)
445+
require.NoError(t, couchdb.ResetDB(db, consts.Permissions))
442446

443447
const sharingID = "sharing-duplicate-interact-permissions"
444448
perms := Permission{
@@ -450,7 +454,7 @@ func TestGetForShareInteractRepairsDuplicateDocs(t *testing.T) {
450454
}},
451455
}
452456

453-
err := couchdb.CreateDoc(testPrefix, &Permission{
457+
err := couchdb.CreateDoc(db, &Permission{
454458
Type: TypeShareInteract,
455459
Permissions: perms.Permissions,
456460
Codes: map[string]string{
@@ -459,7 +463,7 @@ func TestGetForShareInteractRepairsDuplicateDocs(t *testing.T) {
459463
SourceID: consts.Sharings + "/" + sharingID,
460464
})
461465
require.NoError(t, err)
462-
err = couchdb.CreateDoc(testPrefix, &Permission{
466+
err = couchdb.CreateDoc(db, &Permission{
463467
Type: TypeShareInteract,
464468
Permissions: perms.Permissions,
465469
Codes: map[string]string{
@@ -469,27 +473,27 @@ func TestGetForShareInteractRepairsDuplicateDocs(t *testing.T) {
469473
})
470474
require.NoError(t, err)
471475

472-
repaired, err := GetForShareInteract(testPrefix, sharingID)
476+
repaired, err := GetForShareInteract(db, sharingID)
473477
require.NoError(t, err)
474478
require.Equal(t, ShareInteractPermissionID(sharingID), repaired.ID())
475479
require.Equal(t, map[string]string{
476480
"alice@example.test": "alice-token",
477481
"bob@example.test": "bob-token",
478482
}, repaired.Codes)
479483

480-
all, err := getShareInteractPermissions(testPrefix, sharingID)
484+
all, err := getShareInteractPermissions(db, sharingID)
481485
require.NoError(t, err)
482486
require.Len(t, all, 1)
483487
require.Equal(t, ShareInteractPermissionID(sharingID), all[0].ID())
484488

485-
repaired, err = GetForShareInteract(testPrefix, sharingID)
489+
repaired, err = GetForShareInteract(db, sharingID)
486490
require.NoError(t, err)
487491
require.Equal(t, map[string]string{
488492
"alice@example.test": "alice-token",
489493
"bob@example.test": "bob-token",
490494
}, repaired.Codes)
491495

492-
all, err = getShareInteractPermissions(testPrefix, sharingID)
496+
all, err = getShareInteractPermissions(db, sharingID)
493497
require.NoError(t, err)
494498
require.Len(t, all, 1)
495499
}
@@ -500,7 +504,8 @@ func TestGetForShareInteractRepairsExpiredDuplicateDocs(t *testing.T) {
500504
}
501505

502506
config.UseTestFile(t)
503-
require.NoError(t, couchdb.ResetDB(testPrefix, consts.Permissions))
507+
db := testPrefix(t)
508+
require.NoError(t, couchdb.ResetDB(db, consts.Permissions))
504509

505510
const sharingID = "sharing-expired-duplicate-interact-permissions"
506511
rules := Set{{
@@ -511,7 +516,7 @@ func TestGetForShareInteractRepairsExpiredDuplicateDocs(t *testing.T) {
511516
}}
512517
expiredAt := time.Now().Add(-time.Hour).Format(time.RFC3339)
513518

514-
err := couchdb.CreateNamedDoc(testPrefix, &Permission{
519+
err := couchdb.CreateNamedDoc(db, &Permission{
515520
PID: ShareInteractPermissionID(sharingID),
516521
Type: TypeShareInteract,
517522
Permissions: rules,
@@ -521,7 +526,7 @@ func TestGetForShareInteractRepairsExpiredDuplicateDocs(t *testing.T) {
521526
SourceID: consts.Sharings + "/" + sharingID,
522527
})
523528
require.NoError(t, err)
524-
err = couchdb.CreateDoc(testPrefix, &Permission{
529+
err = couchdb.CreateDoc(db, &Permission{
525530
Type: TypeShareInteract,
526531
Permissions: rules,
527532
Codes: map[string]string{
@@ -532,14 +537,14 @@ func TestGetForShareInteractRepairsExpiredDuplicateDocs(t *testing.T) {
532537
})
533538
require.NoError(t, err)
534539

535-
repaired, err := GetForShareInteract(testPrefix, sharingID)
540+
repaired, err := GetForShareInteract(db, sharingID)
536541
require.NoError(t, err)
537542
require.Equal(t, ShareInteractPermissionID(sharingID), repaired.ID())
538543
require.Equal(t, map[string]string{
539544
"alice@example.test": "alice-token",
540545
}, repaired.Codes)
541546

542-
all, err := getShareInteractPermissions(testPrefix, sharingID)
547+
all, err := getShareInteractPermissions(db, sharingID)
543548
require.NoError(t, err)
544549
require.Len(t, all, 1)
545550
require.Equal(t, ShareInteractPermissionID(sharingID), all[0].ID())
@@ -551,7 +556,8 @@ func TestCreateShareInteractSetUsesCanonicalDoc(t *testing.T) {
551556
}
552557

553558
config.UseTestFile(t)
554-
require.NoError(t, couchdb.ResetDB(testPrefix, consts.Permissions))
559+
db := testPrefix(t)
560+
require.NoError(t, couchdb.ResetDB(db, consts.Permissions))
555561

556562
const sharingID = "sharing-canonical-interact-permissions"
557563
md := metadata.New()
@@ -566,13 +572,13 @@ func TestCreateShareInteractSetUsesCanonicalDoc(t *testing.T) {
566572
Metadata: md,
567573
}
568574

569-
first, err := CreateShareInteractSet(testPrefix, sharingID, map[string]string{
575+
first, err := CreateShareInteractSet(db, sharingID, map[string]string{
570576
"alice@example.test": "alice-token",
571577
}, perms)
572578
require.NoError(t, err)
573579
require.Equal(t, ShareInteractPermissionID(sharingID), first.ID())
574580

575-
second, err := CreateShareInteractSet(testPrefix, sharingID, map[string]string{
581+
second, err := CreateShareInteractSet(db, sharingID, map[string]string{
576582
"bob@example.test": "bob-token",
577583
}, perms)
578584
require.NoError(t, err)
@@ -585,7 +591,7 @@ func TestCreateShareInteractSetUsesCanonicalDoc(t *testing.T) {
585591
require.NotNil(t, second.Metadata)
586592
require.True(t, second.Metadata.UpdatedAt.After(first.Metadata.UpdatedAt))
587593

588-
all, err := getShareInteractPermissions(testPrefix, sharingID)
594+
all, err := getShareInteractPermissions(db, sharingID)
589595
require.NoError(t, err)
590596
require.Len(t, all, 1)
591597
require.Equal(t, ShareInteractPermissionID(sharingID), all[0].ID())

model/sharing/sharing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ func (s *Sharing) GetInteractCode(inst *instance.Instance, member *Member, membe
400400
if stored, ok := updated.Codes[key]; ok {
401401
return stored, nil
402402
}
403-
return code, nil
403+
return "", fmt.Errorf("share-interact code for %s was not stored in sharing %s", key, s.SID)
404404
}
405405

406406
func (s *Sharing) createInteractPermissions(inst *instance.Instance, m *Member) (string, error) {

pkg/utils/retry.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package utils
22

33
import (
44
"context"
5-
"math/rand"
5+
"math/rand/v2"
66
"time"
77
)
88

@@ -19,7 +19,8 @@ type RetryOptions struct {
1919
// MaxDelay caps the exponential backoff delay before jitter is applied.
2020
MaxDelay time.Duration
2121
// JitterFactor adds a random delay between 0 and the current backoff delay
22-
// multiplied by JitterFactor. For example, 0.25 adds up to 25% jitter.
22+
// multiplied by JitterFactor. This jitter is one-sided: it can only extend
23+
// the delay, never shorten it. For example, 0.25 adds up to 25% jitter.
2324
// Values lower than or equal to 0 disable jitter.
2425
JitterFactor float64
2526
// ShouldRetry can be used to retry only some errors. When nil, every
@@ -109,14 +110,14 @@ func backoffDelay(initial time.Duration, attempt int, maxDelay time.Duration) ti
109110
delay := initial
110111
for i := 0; i < attempt; i++ {
111112
if delay > maxRetryDelay/2 {
112-
delay = maxRetryDelay
113-
} else {
114-
delay *= 2
115-
}
116-
if maxDelay > 0 && delay > maxDelay {
117-
return maxDelay
113+
return cappedDelay(maxRetryDelay, maxDelay)
118114
}
115+
delay *= 2
119116
}
117+
return cappedDelay(delay, maxDelay)
118+
}
119+
120+
func cappedDelay(delay time.Duration, maxDelay time.Duration) time.Duration {
120121
if maxDelay > 0 && delay > maxDelay {
121122
return maxDelay
122123
}
@@ -141,7 +142,7 @@ func addJitter(delay time.Duration, jitterFactor float64) time.Duration {
141142
if maxJitter <= 0 {
142143
return delay
143144
}
144-
jitter := time.Duration(rand.Int63n(int64(maxJitter)))
145+
jitter := time.Duration(rand.Int64N(int64(maxJitter)))
145146
if maxRetryDelay-delay < jitter {
146147
return maxRetryDelay
147148
}

0 commit comments

Comments
 (0)