@@ -14,6 +14,7 @@ import (
1414 "github.com/cozy/cozy-stack/pkg/couchdb"
1515 "github.com/cozy/cozy-stack/pkg/couchdb/mango"
1616 "github.com/cozy/cozy-stack/pkg/crypto"
17+ "github.com/cozy/cozy-stack/pkg/logger"
1718 "github.com/cozy/cozy-stack/pkg/metadata"
1819 "github.com/cozy/cozy-stack/pkg/prefixer"
1920 "github.com/labstack/echo/v4"
@@ -247,6 +248,143 @@ func GetForShareInteract(db prefixer.Prefixer, sharingID string) (*Permission, e
247248 return getFromSource (db , TypeShareInteract , consts .Sharings , sharingID )
248249}
249250
251+ // ShareInteractPermissionID returns the canonical permission document ID for a
252+ // share-interact permission set.
253+ func ShareInteractPermissionID (sharingID string ) string {
254+ return TypeShareInteract + "-" + sharingID
255+ }
256+
257+ func getShareInteractPermissions (db prefixer.Prefixer , sharingID string ) ([]Permission , error ) {
258+ var res []Permission
259+ req := couchdb.FindRequest {
260+ UseIndex : "by-source-and-type" ,
261+ Selector : mango .And (
262+ mango .Equal ("type" , TypeShareInteract ),
263+ mango .Equal ("source_id" , consts .Sharings + "/" + sharingID ),
264+ ),
265+ Limit : 1000 ,
266+ }
267+ err := couchdb .FindDocs (db , consts .Permissions , & req , & res )
268+ if err != nil {
269+ return nil , err
270+ }
271+ return res , nil
272+ }
273+
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 )
281+ if err == nil && ! retry {
282+ return perm , nil
283+ }
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+ }
294+ }
295+
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
300+ }
301+ if len (perms ) == 0 {
302+ return nil , false , & couchdb.Error {
303+ StatusCode : http .StatusNotFound ,
304+ Name : "not_found" ,
305+ Reason : fmt .Sprintf ("no permission doc for %v" , sharingID ),
306+ }
307+ }
308+
309+ canonicalID := ShareInteractPermissionID (sharingID )
310+ merged := & Permission {
311+ PID : canonicalID ,
312+ Type : TypeShareInteract ,
313+ SourceID : consts .Sharings + "/" + sharingID ,
314+ Codes : make (map [string ]string ),
315+ }
316+ hasUsablePermission := false
317+ hasCanonical := false
318+ duplicates := make ([]* Permission , 0 , len (perms ))
319+ for i := range perms {
320+ p := & perms [i ]
321+ if p .Expired () {
322+ continue
323+ }
324+
325+ hasUsablePermission = true
326+ if p .ID () == canonicalID {
327+ hasCanonical = true
328+ merged .SetRev (p .Rev ())
329+ } else {
330+ duplicates = append (duplicates , p )
331+ }
332+
333+ if len (merged .Permissions ) == 0 && len (p .Permissions ) > 0 {
334+ merged .Permissions = p .Clone ().(* Permission ).Permissions
335+ }
336+ if merged .Metadata == nil && p .Metadata != nil {
337+ merged .Metadata = p .Metadata .Clone ()
338+ }
339+ if merged .ExpiresAt == nil && p .ExpiresAt != nil {
340+ merged .ExpiresAt = p .ExpiresAt
341+ }
342+
343+ for key , code := range p .Codes {
344+ if key == "" {
345+ continue
346+ }
347+ if existing , ok := merged .Codes [key ]; ok && existing != code {
348+ logger .WithDomain (db .DomainName ()).WithNamespace ("permissions" ).
349+ Warnf ("conflicting share-interact code for %s in sharing %s" , key , sharingID )
350+ continue
351+ }
352+ merged .Codes [key ] = code
353+ }
354+ }
355+ if ! hasUsablePermission {
356+ return nil , false , ErrExpiredToken
357+ }
358+
359+ if ! hasCanonical {
360+ if err := couchdb .CreateNamedDoc (db , merged ); err != nil {
361+ if couchdb .IsConflictError (err ) || couchdb .IsFileExists (err ) {
362+ return nil , true , err
363+ }
364+ return nil , false , err
365+ }
366+ } else if err := couchdb .UpdateDoc (db , merged ); err != nil {
367+ if couchdb .IsConflictError (err ) {
368+ return nil , true , err
369+ }
370+ return nil , false , err
371+ }
372+
373+ for _ , p := range duplicates {
374+ if err := couchdb .DeleteDoc (db , p ); err != nil {
375+ if couchdb .IsNotFoundError (err ) {
376+ continue
377+ }
378+ if couchdb .IsConflictError (err ) {
379+ return nil , true , err
380+ }
381+ return nil , false , err
382+ }
383+ }
384+
385+ return merged , false , nil
386+ }
387+
250388func getFromSource (db prefixer.Prefixer , permType , docType , slug string ) (* Permission , error ) {
251389 var res []Permission
252390 req := couchdb.FindRequest {
0 commit comments