Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement eos-compliant app locks #4264

Merged
merged 1 commit into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/unreleased/eos-locks.md
Original file line number Diff line number Diff line change
@@ -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
211 changes: 89 additions & 122 deletions pkg/storage/utils/eosfs/eosfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "/") {
Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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{
Expand All @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down