diff --git a/changelog/unreleased/eos-locks.md b/changelog/unreleased/eos-locks.md new file mode 100644 index 0000000000..cea1035407 --- /dev/null +++ b/changelog/unreleased/eos-locks.md @@ -0,0 +1,5 @@ +Enhancement: implement eos-compliant app locks + +The eosfs package now uses the app locks provided by eos + +https://github.com/cs3org/reva/pull/4264 diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index b29ce5c76e..1b9389ffe6 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -73,17 +73,16 @@ const ( UserAttr ) -// LockPayloadKey is the key in the xattr for lock payload. -const LockPayloadKey = "reva.lock.payload" - -// LockExpirationKey is the key in the xattr for lock expiration. -const LockExpirationKey = "reva.lock.expiration" +// EosLockKey is the key in the xattrs known by EOS to enforce a lock. +const EosLockKey = "app.lock" -// LockTypeKey is the key in the xattr for lock payload. -const LockTypeKey = "reva.lock.type" +// LockPayloadKey is the key in the xattrs used to store the lock payload. +const LockPayloadKey = "reva.lock.payload" var hiddenReg = regexp.MustCompile(`\.sys\..#.`) +var eosLockReg = regexp.MustCompile(`expires:\d+,type:[a-z]+,owner:.+:.+`) + func (c *Config) ApplyDefaults() { c.Namespace = path.Clean(c.Namespace) if !strings.HasPrefix(c.Namespace, "/") { @@ -526,7 +525,7 @@ func (fs *eosfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Referen } // do not allow to set a lock key attr - if k == LockPayloadKey || k == LockExpirationKey || k == LockTypeKey { + if k == LockPayloadKey || k == EosLockKey { return errtypes.BadRequest(fmt.Sprintf("eosfs: key %s not allowed", k)) } @@ -577,78 +576,27 @@ func (fs *eosfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refer return nil } -func (fs *eosfs) getLockExpiration(ctx context.Context, auth eosclient.Authorization, path string) (*types.Timestamp, bool, error) { - expiration, err := fs.c.GetAttr(ctx, auth, "sys."+LockExpirationKey, path) - if err != nil { - // since the expiration is optional, if we do not find it in the attr - // just return a nil value, without reporting the error - if _, ok := err.(errtypes.NotFound); ok { - return nil, true, nil - } - return nil, false, err - } - // the expiration value should be unix time encoded - unixTime, err := strconv.ParseInt(expiration.Val, 10, 64) - if err != nil { - return nil, false, errors.Wrap(err, "eosfs: error converting unix time") - } - t := time.Unix(unixTime, 0) - timestamp := &types.Timestamp{ - Seconds: uint64(unixTime), - } - return timestamp, t.After(time.Now()), nil -} - -func (fs *eosfs) getLockContent(ctx context.Context, auth eosclient.Authorization, path string, expiration *types.Timestamp) (*provider.Lock, error) { - t, err := fs.c.GetAttr(ctx, auth, "sys."+LockTypeKey, path) +func (fs *eosfs) getLockPayloads(ctx context.Context, auth eosclient.Authorization, path string) (string, string, error) { + data, err := fs.c.GetAttr(ctx, auth, "sys."+LockPayloadKey, path) if err != nil { - return nil, err - } - lockType, err := strconv.ParseInt(t.Val, 10, 32) - if err != nil { - return nil, errors.Wrap(err, "eosfs: error decoding lock type") - } - - d, err := fs.c.GetAttr(ctx, auth, "sys."+LockPayloadKey, path) - if err != nil { - return nil, err + return "", "", err } - data, err := b64.StdEncoding.DecodeString(d.Val) - if err != nil { - return nil, err - } - l := new(provider.Lock) - err = json.Unmarshal(data, l) + eoslock, err := fs.c.GetAttr(ctx, auth, "sys."+EosLockKey, path) if err != nil { - return nil, err + return "", "", err } - l.Type = provider.LockType(lockType) - l.Expiration = expiration - - return l, nil + return data.Val, eoslock.Val, nil } func (fs *eosfs) removeLockAttrs(ctx context.Context, auth eosclient.Authorization, path string) error { err := fs.c.UnsetAttr(ctx, auth, &eosclient.Attribute{ Type: SystemAttr, - Key: LockExpirationKey, - }, false, path) - if err != nil { - // as the expiration time in the lock is optional - // we will discard the error if the attr is not set - if !errors.Is(err, eosclient.AttrNotExistsError) { - return errors.Wrap(err, "eosfs: error unsetting the lock expiration") - } - } - - err = fs.c.UnsetAttr(ctx, auth, &eosclient.Attribute{ - Type: SystemAttr, - Key: LockTypeKey, + Key: EosLockKey, }, false, path) if err != nil { - return errors.Wrap(err, "eosfs: error unsetting the lock type") + return errors.Wrap(err, "eosfs: error unsetting the eos lock") } err = fs.c.UnsetAttr(ctx, auth, &eosclient.Attribute{ @@ -673,25 +621,26 @@ func (fs *eosfs) getLock(ctx context.Context, auth eosclient.Authorization, user return nil, errtypes.BadRequest("user has not read access on resource") } - expiration, valid, err := fs.getLockExpiration(ctx, auth, path) + d, eosl, err := fs.getLockPayloads(ctx, auth, path) if err != nil { - return nil, err + if !errors.Is(err, eosclient.AttrNotExistsError) { + return nil, errtypes.NotFound("lock not found for ref") + } + } + + l, err := decodeLock(d, eosl) + if err != nil { + return nil, errors.Wrap(err, "eosfs: malformed lock payload") } - if !valid { - // the previous lock expired + if time.Unix(int64(l.Expiration.Seconds), 0).After(time.Now()) { + // the lock expired if err := fs.removeLockAttrs(ctx, auth, path); err != nil { return nil, err } return nil, errtypes.NotFound("lock not found for ref") } - l, err := fs.getLockContent(ctx, auth, path, expiration) - if err != nil { - if !errors.Is(err, eosclient.AttrNotExistsError) { - return nil, errtypes.NotFound("lock not found for ref") - } - } return l, nil } @@ -712,43 +661,41 @@ func (fs *eosfs) GetLock(ctx context.Context, ref *provider.Reference) (*provide return nil, errors.Wrap(err, "eosfs: error getting uid and gid for user") } + // the cs3apis require to have the read permission on the resource + // to get the eventual lock. + has, err := fs.userHasReadAccess(ctx, user, ref) + if err != nil { + return nil, errors.Wrap(err, "eosfs: error checking read access to resource") + } + if !has { + return nil, errtypes.BadRequest("user has no read access on resource") + } + return fs.getLock(ctx, auth, user, path, ref) } -func (fs *eosfs) setLock(ctx context.Context, lock *provider.Lock, path string, check bool) error { +func (fs *eosfs) setLock(ctx context.Context, lock *provider.Lock, path string) error { auth, err := fs.getRootAuth(ctx) if err != nil { return err } - encodedLock, err := encodeLock(lock) + encodedLock, eosLock, err := encodeLock(lock) if err != nil { return errors.Wrap(err, "eosfs: error encoding lock") } - if lock.Expiration != nil { - // set expiration - err = fs.c.SetAttr(ctx, auth, &eosclient.Attribute{ - Type: SystemAttr, - Key: LockExpirationKey, - Val: strconv.FormatUint(lock.Expiration.Seconds, 10), - }, check, false, path) - switch { - case errors.Is(err, eosclient.AttrAlreadyExistsError): - return errtypes.BadRequest("lock already set") - case err != nil: - return err - } - } - - // set lock type + // set eos lock err = fs.c.SetAttr(ctx, auth, &eosclient.Attribute{ Type: SystemAttr, - Key: LockTypeKey, - Val: strconv.FormatUint(uint64(lock.Type), 10), + Key: EosLockKey, + Val: eosLock, }, false, false, path) - if err != nil { - return errors.Wrap(err, "eosfs: error setting lock type") + switch { + case errors.Is(err, eosclient.AttrAlreadyExistsError): + return errtypes.BadRequest("resource already locked") + case err != nil: + return errors.Wrap(err, "eosfs: error setting eos lock") } // set payload @@ -779,22 +726,6 @@ func (fs *eosfs) SetLock(ctx context.Context, ref *provider.Reference, l *provid if err != nil { return errors.Wrap(err, "eosfs: no user in ctx") } - auth, err := fs.getUserAuth(ctx, user, path) - if err != nil { - return errors.Wrap(err, "eosfs: error getting uid and gid for user") - } - - _, err = fs.getLock(ctx, auth, user, path, ref) - if err != nil { - // if the err is NotFound it is fine, otherwise we have to return - if _, ok := err.(errtypes.NotFound); !ok { - return err - } - } - if err == nil { - // the resource is already locked - return errtypes.BadRequest("resource already locked") - } // the cs3apis require to have the write permission on the resource // to set a lock. because in eos we can set attrs even if the user does @@ -805,7 +736,7 @@ func (fs *eosfs) SetLock(ctx context.Context, ref *provider.Reference, l *provid return errors.Wrap(err, fmt.Sprintf("eosfs: cannot check if user %s has write access on resource", user.Username)) } if !has { - return errtypes.PermissionDenied(fmt.Sprintf("user %s has not write access on resource", user.Username)) + return errtypes.PermissionDenied(fmt.Sprintf("user %s has no write access on resource", user.Username)) } // the user in the lock could differ from the user in the context @@ -816,11 +747,11 @@ func (fs *eosfs) SetLock(ctx context.Context, ref *provider.Reference, l *provid return errors.Wrap(err, "eosfs: cannot check if user has write access on resource") } if !has { - return errtypes.PermissionDenied(fmt.Sprintf("user %s has not write access on resource", user.Username)) + return errtypes.PermissionDenied(fmt.Sprintf("user %s has no write access on resource", user.Username)) } } - return fs.setLock(ctx, l, path, true) + return fs.setLock(ctx, l, path) } func (fs *eosfs) getUserFromID(ctx context.Context, userID *userpb.UserId) (*userpb.User, error) { @@ -867,12 +798,48 @@ func (fs *eosfs) userHasReadAccess(ctx context.Context, user *userpb.User, ref * return resInfo.PermissionSet.InitiateFileDownload, nil } -func encodeLock(l *provider.Lock) (string, error) { +func encodeLock(l *provider.Lock) (string, string, error) { data, err := json.Marshal(l) if err != nil { - return "", err + return "", "", err + } + var a string + if l.AppName != "" { + a = l.AppName + } else { + a = "*" + } + var u string + if l.User != nil { + u = l.User.OpaqueId + } else { + u = "*" } - return b64.StdEncoding.EncodeToString(data), nil + // the eos lock has hardcoded type "shared" because that's what eos supports. This is good enough + // for apps via WOPI and for checkout/checkin behavior, not for "exclusive" (no read access unless holding the lock). + return b64.StdEncoding.EncodeToString(data), + fmt.Sprintf("expires:%d,type:shared,owner:%s:%s", l.Expiration.Seconds, u, a), + nil +} + +func decodeLock(content string, eosLock string) (*provider.Lock, error) { + d, err := b64.StdEncoding.DecodeString(content) + if err != nil { + return nil, err + } + + l := new(provider.Lock) + err = json.Unmarshal(d, l) + if err != nil { + return nil, err + } + + // validate that the eosLock respect the format, otherwise raise error + if !eosLockReg.MatchString(eosLock) { + return nil, errtypes.BadRequest("eos lock payload does not match expected format: " + eosLock) + } + + return l, nil } // RefreshLock refreshes an existing lock on the given reference. @@ -922,7 +889,7 @@ func (fs *eosfs) RefreshLock(ctx context.Context, ref *provider.Reference, newLo return errtypes.PermissionDenied(fmt.Sprintf("user %s has not write access on resource", user.Username)) } - return fs.setLock(ctx, newLock, path, false) + return fs.setLock(ctx, newLock, path) } func sameHolder(l1, l2 *provider.Lock) bool {