33package permission
44
55import (
6+ "context"
67 "encoding/json"
78 "fmt"
89 "net/http"
910 "strings"
1011 "time"
1112
1213 build "github.com/cozy/cozy-stack/pkg/config"
14+ stackconfig "github.com/cozy/cozy-stack/pkg/config/config"
1315 "github.com/cozy/cozy-stack/pkg/consts"
1416 "github.com/cozy/cozy-stack/pkg/couchdb"
1517 "github.com/cozy/cozy-stack/pkg/couchdb/mango"
1618 "github.com/cozy/cozy-stack/pkg/crypto"
1719 "github.com/cozy/cozy-stack/pkg/logger"
1820 "github.com/cozy/cozy-stack/pkg/metadata"
1921 "github.com/cozy/cozy-stack/pkg/prefixer"
22+ "github.com/cozy/cozy-stack/pkg/utils"
2023 "github.com/labstack/echo/v4"
2124)
2225
@@ -243,9 +246,17 @@ func GetForSharePreview(db prefixer.Prefixer, sharingID string) (*Permission, er
243246}
244247
245248// GetForShareInteract retrieves the Permission doc for a given sharing to
246- // read/write a note
249+ // read/write a note. It may repair legacy duplicate share-interact permission
250+ // docs as part of the read by creating/updating the canonical permission doc
251+ // and deleting duplicate legacy docs.
247252func GetForShareInteract (db prefixer.Prefixer , sharingID string ) (* Permission , error ) {
248- return getFromSource (db , TypeShareInteract , consts .Sharings , sharingID )
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+
259+ return getOrRepairShareInteractPermissions (db , sharingID )
249260}
250261
251262// ShareInteractPermissionID returns the canonical permission document ID for a
@@ -254,6 +265,10 @@ func ShareInteractPermissionID(sharingID string) string {
254265 return TypeShareInteract + "-" + sharingID
255266}
256267
268+ func shareInteractLockName (sharingID string ) string {
269+ return "permissions/share-interact/" + sharingID
270+ }
271+
257272func getShareInteractPermissions (db prefixer.Prefixer , sharingID string ) ([]Permission , error ) {
258273 var res []Permission
259274 req := couchdb.FindRequest {
@@ -271,33 +286,44 @@ func getShareInteractPermissions(db prefixer.Prefixer, sharingID string) ([]Perm
271286 return res , nil
272287}
273288
274- // RepairShareInteractPermissions merges duplicate share-interact permission
275- // documents for a sharing into the canonical document and deletes the legacy
276- // duplicates.
277- func RepairShareInteractPermissions (db prefixer.Prefixer , sharingID string ) (* Permission , error ) {
278- var lastErr error
279- for attempt := 0 ; attempt < 5 ; attempt ++ {
280- perm , retry , err := repairShareInteractPermissions (db , sharingID )
289+ func getOrRepairShareInteractPermissions (db prefixer.Prefixer , sharingID string ) (* Permission , error ) {
290+ return utils .RetryWithBackoffValue (context .Background (), shareInteractRetryOptions (), func () (* Permission , error ) {
291+ perms , err := getShareInteractPermissions (db , sharingID )
292+ if err != nil {
293+ return nil , err
294+ }
295+ if canonical , needsRepair , err := shareInteractRepairState (perms , sharingID ); err != nil || ! needsRepair {
296+ return canonical , err
297+ }
298+
299+ perm , retry , err := repairShareInteractPermissions (db , sharingID , perms )
281300 if err == nil && ! retry {
282301 return perm , nil
283302 }
284- lastErr = err
285- }
286- if lastErr != nil {
287- return nil , lastErr
288- }
289- return nil , & couchdb.Error {
290- StatusCode : http .StatusConflict ,
291- Name : "conflict" ,
292- Reason : "could not repair share-interact permissions after retries" ,
293- }
303+ if retry && err == nil {
304+ return nil , & couchdb.Error {
305+ StatusCode : http .StatusConflict ,
306+ Name : "conflict" ,
307+ Reason : "could not repair share-interact permissions after retries" ,
308+ }
309+ }
310+ return nil , err
311+ })
294312}
295313
296- func repairShareInteractPermissions (db prefixer.Prefixer , sharingID string ) (* Permission , bool , error ) {
297- perms , err := getShareInteractPermissions (db , sharingID )
298- if err != nil {
299- return nil , false , err
314+ func shareInteractRetryOptions () utils.RetryOptions {
315+ return utils.RetryOptions {
316+ Attempts : 5 ,
317+ Delay : 10 * time .Millisecond ,
318+ MaxDelay : 100 * time .Millisecond ,
319+ JitterFactor : 0.25 ,
320+ ShouldRetry : func (err error ) bool {
321+ return couchdb .IsConflictError (err ) || couchdb .IsFileExists (err )
322+ },
300323 }
324+ }
325+
326+ func shareInteractRepairState (perms []Permission , sharingID string ) (* Permission , bool , error ) {
301327 if len (perms ) == 0 {
302328 return nil , false , & couchdb.Error {
303329 StatusCode : http .StatusNotFound ,
@@ -306,6 +332,28 @@ func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string) (*Pe
306332 }
307333 }
308334
335+ canonicalID := ShareInteractPermissionID (sharingID )
336+ var canonical * Permission
337+ usable := 0
338+ for i := range perms {
339+ if perms [i ].Expired () {
340+ continue
341+ }
342+ usable ++
343+ if perms [i ].ID () == canonicalID {
344+ canonical = & perms [i ]
345+ }
346+ }
347+ if usable == 0 {
348+ return nil , false , ErrExpiredToken
349+ }
350+ if usable == 1 && canonical != nil {
351+ return canonical , false , nil
352+ }
353+ return nil , true , nil
354+ }
355+
356+ func repairShareInteractPermissions (db prefixer.Prefixer , sharingID string , perms []Permission ) (* Permission , bool , error ) {
309357 canonicalID := ShareInteractPermissionID (sharingID )
310358 merged := & Permission {
311359 PID : canonicalID ,
@@ -318,15 +366,17 @@ func repairShareInteractPermissions(db prefixer.Prefixer, sharingID string) (*Pe
318366 duplicates := make ([]* Permission , 0 , len (perms ))
319367 for i := range perms {
320368 p := & perms [i ]
369+ if p .ID () == canonicalID {
370+ hasCanonical = true
371+ // If the canonical doc is expired, update that doc ID but rebuild its content from non-expired docs.
372+ merged .SetRev (p .Rev ())
373+ }
321374 if p .Expired () {
322375 continue
323376 }
324377
325378 hasUsablePermission = true
326- if p .ID () == canonicalID {
327- hasCanonical = true
328- merged .SetRev (p .Rev ())
329- } else {
379+ if p .ID () != canonicalID {
330380 duplicates = append (duplicates , p )
331381 }
332382
@@ -743,21 +793,75 @@ func CreateSharePreviewSet(db prefixer.Prefixer, sharingID string, codes, shortc
743793 return doc , nil
744794}
745795
746- // CreateShareInteractSet creates a Permission doc for reading/writing a note
747- // inside a sharing
796+ // CreateShareInteractSet creates or updates the Permission doc for reading and
797+ // writing a note inside a sharing. When subdoc.Permissions is not empty, it
798+ // replaces the existing permission rules with that full set.
748799func CreateShareInteractSet (db prefixer.Prefixer , sharingID string , codes map [string ]string , subdoc Permission ) (* Permission , error ) {
749800 doc := & Permission {
801+ PID : ShareInteractPermissionID (sharingID ),
750802 Type : TypeShareInteract ,
751803 Permissions : subdoc .Permissions ,
752804 Codes : codes ,
753805 SourceID : consts .Sharings + "/" + sharingID ,
754806 Metadata : subdoc .Metadata ,
755807 }
756- err := couchdb .CreateDoc (db , doc )
757- if err != nil {
808+
809+ if doc .Codes == nil {
810+ doc .Codes = make (map [string ]string )
811+ }
812+
813+ mu := stackconfig .Lock ().ReadWrite (db , shareInteractLockName (sharingID ))
814+ if err := mu .Lock (); err != nil {
758815 return nil , err
759816 }
760- return doc , nil
817+ defer mu .Unlock ()
818+
819+ return utils .RetryWithBackoffValue (context .Background (), shareInteractRetryOptions (), func () (* Permission , error ) {
820+ existing , err := getOrRepairShareInteractPermissions (db , sharingID )
821+ if err != nil && ! couchdb .IsNotFoundError (err ) {
822+ return nil , err
823+ }
824+ if err == nil {
825+ merged := existing .Clone ().(* Permission )
826+ merged .Type = TypeShareInteract
827+ merged .SourceID = consts .Sharings + "/" + sharingID
828+ merged .SetID (ShareInteractPermissionID (sharingID ))
829+ if len (subdoc .Permissions ) > 0 {
830+ // Callers pass the complete interact rule set, so replace instead of merging rules.
831+ merged .Permissions = subdoc .Permissions
832+ }
833+ if merged .Metadata == nil && subdoc .Metadata != nil {
834+ merged .Metadata = subdoc .Metadata .Clone ()
835+ }
836+ if merged .Metadata != nil {
837+ merged .Metadata .ChangeUpdatedAt ()
838+ }
839+ if merged .Codes == nil {
840+ merged .Codes = make (map [string ]string )
841+ }
842+ for key , code := range codes {
843+ if key == "" {
844+ continue
845+ }
846+ if existingCode , ok := merged .Codes [key ]; ok && existingCode != code {
847+ logger .WithDomain (db .DomainName ()).WithNamespace ("permissions" ).
848+ Warnf ("keeping existing share-interact code for %s in sharing %s" , key , sharingID )
849+ continue
850+ }
851+ merged .Codes [key ] = code
852+ }
853+ if err := couchdb .UpdateDoc (db , merged ); err != nil {
854+ return nil , err
855+ }
856+ return merged , nil
857+ }
858+
859+ err = couchdb .CreateNamedDoc (db , doc )
860+ if err == nil {
861+ return doc , nil
862+ }
863+ return nil , err
864+ })
761865}
762866
763867// ForceWebapp creates or updates a Permission doc for a given webapp
0 commit comments