Skip to content

Commit

Permalink
Implement eos-compliant app locks
Browse files Browse the repository at this point in the history
  • Loading branch information
glpatcern committed Oct 16, 2023
1 parent 084c1b5 commit ffc4bc8
Showing 1 changed file with 81 additions and 118 deletions.
199 changes: 81 additions & 118 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)
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, false, errors.Wrap(err, "eosfs: error converting unix time")
}
t := time.Unix(unixTime, 0)
timestamp := &types.Timestamp{
Seconds: uint64(unixTime),
return "", "", err
}
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)
if err != nil {
return nil, err
}
lockType, err := strconv.ParseInt(t.Val, 10, 32)
eoslock, err := fs.c.GetAttr(ctx, auth, "sys."+EosLockKey, path)
if err != nil {
return nil, errors.Wrap(err, "eosfs: error decoding lock type")
return "", "", err
}

d, err := fs.c.GetAttr(ctx, auth, "sys."+LockPayloadKey, path)
if err != nil {
return nil, err
}

data, err := b64.StdEncoding.DecodeString(d.Val)
if err != nil {
return nil, err
}
l := new(provider.Lock)
err = json.Unmarshal(data, l)
if err != nil {
return nil, 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")
}
}

if !valid {
// the previous lock expired
l, err := decodeLock(d, eosl)
if err != nil {
return nil, errors.Wrap(err, "eosfs: malformed lock payload")
}

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,6 +661,16 @@ 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)
}

Expand All @@ -721,34 +680,19 @@ func (fs *eosfs) setLock(ctx context.Context, lock *provider.Lock, path string,
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")
return errors.Wrap(err, "eosfs: error setting eos lock")
}

// set payload
Expand Down Expand Up @@ -779,22 +723,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 +733,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,7 +744,7 @@ 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))
}
}

Expand Down Expand Up @@ -867,12 +795,47 @@ 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 = "*"
}
// 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
}
return b64.StdEncoding.EncodeToString(data), nil

l := new(provider.Lock)
err = json.Unmarshal(d, l)
if err != nil {
return nil, err
}

// Just validate the eosLock respects the format, otherwise raise
if !eosLockReg.MatchString(eosLock) {
return nil, errtypes.NotFound("eos lock payload does not match expected format: " + eosLock)
}
return l, nil
}

// RefreshLock refreshes an existing lock on the given reference.
Expand Down

0 comments on commit ffc4bc8

Please sign in to comment.