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

Lightweight accounts #3348

Merged
merged 21 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from 19 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
8 changes: 8 additions & 0 deletions changelog/unreleased/lightweigh-accounts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Revamp lightweigth accounts

Re-implements the lighweight account scope check, making
it more efficient.
Also, the ACLs for the EOS storage driver for the lw
accounts are set atomically.

https://github.com/cs3org/reva/pull/3348
164 changes: 112 additions & 52 deletions internal/grpc/interceptors/auth/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package auth

import (
"context"
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -78,77 +79,136 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s
return nil
}

case strings.HasPrefix(k, "lightweight"):
if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil {
return nil
}
}
log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k)
}

} else if ref, ok := extractShareRef(req); ok {
// It's a share ref
// The request might be coming from a share created for a lightweight account
// after the token was minted.
log.Info().Msgf("resolving share reference against received shares to verify token scope %+v", ref.String())
for k := range tokenScope {
if strings.HasPrefix(k, "lightweight") {
// Check if this ID is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + ref.GetId().OpaqueId
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
}

shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
log.Warn().Err(err).Msg("error listing received shares")
continue
}
for _, s := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + s.Share.Id.OpaqueId
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)

if ref.GetId() != nil && ref.GetId().OpaqueId == s.Share.Id.OpaqueId {
return nil
}
if key := ref.GetKey(); key != nil && (utils.UserEqual(key.Owner, s.Share.Owner) || utils.UserEqual(key.Owner, s.Share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, s.Share.ResourceId) && utils.GranteeEqual(key.Grantee, s.Share.Grantee) {
return nil
}
}
}
}
if checkLightweightScope(ctx, req, tokenScope, client) {
return nil
}

return errtypes.PermissionDenied("access to resource not allowed within the assigned scope")
}

func resolveLightweightScope(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, user *userpb.User, client gateway.GatewayAPIClient, mgr token.Manager) error {
// Check if this ref is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + getRefKey(ref)
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
func hasLightweightScope(tokenScope map[string]*authpb.Scope) bool {
for scope := range tokenScope {
if strings.HasPrefix(scope, "lightweight") {
return true
}
}
return false
}

shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
return errtypes.InternalError("error listing received shares")
func checkLightweightScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, client gateway.GatewayAPIClient) bool {
if !hasLightweightScope(tokenScope) {
return false
}

for _, share := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + resourceid.OwnCloudResourceIDWrap(share.Share.ResourceId)
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
switch r := req.(type) {
// Viewer role
case *registry.GetStorageProvidersRequest:
return true
case *provider.StatRequest:
return true
case *provider.ListContainerRequest:
return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{
ListContainer: true,
})
case *provider.InitiateFileDownloadRequest:
return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{
InitiateFileDownload: true,
})
case *appprovider.OpenInAppRequest:
return hasPermissions(ctx, client, &provider.Reference{ResourceId: r.ResourceInfo.Id}, &provider.ResourcePermissions{
InitiateFileDownload: true,
})
case *gateway.OpenInAppRequest:
return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{
InitiateFileDownload: true,
})

if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) {
return nil
// Editor role
case *provider.CreateContainerRequest:
parent, err := parentOfResource(ctx, client, r.GetRef())
if err != nil {
return false
}
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{
CreateContainer: true,
})
case *provider.TouchFileRequest:
parent, err := parentOfResource(ctx, client, r.GetRef())
if err != nil {
return false
}
return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{
InitiateFileDownload: true,
})
case *provider.DeleteRequest:
return hasPermissions(ctx, client, r.GetRef(), &provider.ResourcePermissions{
InitiateFileDownload: true,
})
case *provider.MoveRequest:
return hasPermissions(ctx, client, r.Source, &provider.ResourcePermissions{
InitiateFileDownload: true,
}) && hasPermissions(ctx, client, r.Destination, &provider.ResourcePermissions{
InitiateFileUpload: true,
})
case *provider.InitiateFileUploadRequest:
parent, err := parentOfResource(ctx, client, r.GetRef())
if err != nil {
return false
}
return hasPermissions(ctx, client, parent, &provider.ResourcePermissions{
InitiateFileUpload: true,
})
}

return errtypes.PermissionDenied("request is not for a nested resource")
return false
}

func parentOfResource(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference) (*provider.Reference, error) {
if utils.IsAbsolutePathReference(ref) {
parent := filepath.Dir(ref.GetPath())
info, err := stat(ctx, client, &provider.Reference{Path: parent})
if err != nil {
return nil, err
}
return &provider.Reference{ResourceId: info.Id}, nil
}

info, err := stat(ctx, client, ref)
if err != nil {
return nil, err
}
return &provider.Reference{ResourceId: info.ParentId}, nil
}

func stat(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference) (*provider.ResourceInfo, error) {
statRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: ref,
})

switch {
case err != nil:
return nil, err
case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound(statRes.Status.Message)
case statRes.Status.Code != rpc.Code_CODE_OK:
return nil, errtypes.InternalError(statRes.Status.Message)
}

return statRes.Info, nil
}

func hasPermissions(ctx context.Context, client gateway.GatewayAPIClient, ref *provider.Reference, permissionSet *provider.ResourcePermissions) bool {
info, err := stat(ctx, client, ref)
if err != nil {
return false
}
return utils.HasPermissions(info.PermissionSet, permissionSet)
}

func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
Expand Down
2 changes: 2 additions & 0 deletions pkg/auth/scope/lightweight.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ func checkLightweightPath(path string) bool {
"/ocs/v1.php/cloud/user",
"/remote.php/webdav",
"/remote.php/dav/files",
"/thumbnails",
"/app/open",
"/app/new",
"/archiver",
"/dataprovider",
"/data",
"/app/open",
}
for _, p := range paths {
if strings.HasPrefix(path, p) {
Expand Down
91 changes: 28 additions & 63 deletions pkg/eosclient/eosbinary/eosbinary.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import (

const (
versionPrefix = ".sys.v#."
lwShareAttrKey = "reva.lwshare"
userACLEvalKey = "eval.useracl"
favoritesKey = "http://owncloud.org/ns/favorite"
)
Expand Down Expand Up @@ -280,30 +279,6 @@ func (c *Client) AddACL(ctx context.Context, auth, rootAuth eosclient.Authorizat
return err
}

if a.Type == acl.TypeLightweight {
sysACL := ""
aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey]
if ok {
acls, err := acl.Parse(aclStr, acl.ShortTextForm)
if err != nil {
return err
}
err = acls.SetEntry(a.Type, a.Qualifier, a.Permissions)
if err != nil {
return err
}
sysACL = acls.Serialize()
} else {
sysACL = a.CitrineSerialize()
}
sysACLAttr := &eosclient.Attribute{
Type: eosclient.SystemAttr,
Key: lwShareAttrKey,
Val: sysACL,
}
return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path)
}

sysACL := a.CitrineSerialize()
args := []string{"acl", "--sys"}
if finfo.IsDir {
Expand All @@ -330,30 +305,6 @@ func (c *Client) RemoveACL(ctx context.Context, auth, rootAuth eosclient.Authori
return err
}

if a.Type == acl.TypeLightweight {
sysACL := ""
aclStr, ok := finfo.Attrs["sys."+lwShareAttrKey]
if ok {
acls, err := acl.Parse(aclStr, acl.ShortTextForm)
if err != nil {
return err
}
acls.DeleteEntry(a.Type, a.Qualifier)
if err != nil {
return err
}
sysACL = acls.Serialize()
} else {
sysACL = a.CitrineSerialize()
}
sysACLAttr := &eosclient.Attribute{
Type: eosclient.SystemAttr,
Key: lwShareAttrKey,
Val: sysACL,
}
return c.SetAttr(ctx, auth, sysACLAttr, false, finfo.IsDir, path)
}

sysACL := a.CitrineSerialize()
args := []string{"acl", "--sys"}
if finfo.IsDir {
Expand Down Expand Up @@ -645,6 +596,34 @@ func (c *Client) GetAttr(ctx context.Context, auth eosclient.Authorization, key,
return attr, nil
}

// GetAttrs returns all the attributes of a resource
func (c *Client) GetAttrs(ctx context.Context, auth eosclient.Authorization, path string) ([]*eosclient.Attribute, error) {
info, err := c.getRawFileInfoByPath(ctx, auth, path)
if err != nil {
return nil, err
}
if !info.IsDir {
path = getVersionFolder(path)
}

args := []string{"attr", "ls", path}
attrOut, _, err := c.executeEOS(ctx, args, auth)
if err != nil {
return nil, err
}

attrsStr := strings.Split(attrOut, "\n")
attrs := make([]*eosclient.Attribute, 0, len(attrsStr))
for _, line := range attrsStr {
attr, err := deserializeAttribute(line)
if err != nil {
return nil, err
}
attrs = append(attrs, attr)
}
return attrs, nil
}

func deserializeAttribute(attrStr string) (*eosclient.Attribute, error) {
// the string is in the form sys.forced.checksum="adler"
keyValue := strings.SplitN(strings.TrimSpace(attrStr), "=", 2) // keyValue = ["sys.forced.checksum", "\"adler\""]
Expand Down Expand Up @@ -1222,20 +1201,6 @@ func (c *Client) mapToFileInfo(ctx context.Context, kv, attrs map[string]string,
}
}

// Read lightweight ACLs recognized by the sys.reva.lwshare attr
if lwACLStr, ok := attrs["sys."+lwShareAttrKey]; ok {
lwAcls, err := acl.Parse(lwACLStr, acl.ShortTextForm)
if err != nil {
return nil, err
}
for _, e := range lwAcls.Entries {
err = sysACL.SetEntry(e.Type, e.Qualifier, e.Permissions)
if err != nil {
return nil, err
}
}
}

// Read the favorite attr
if parseFavoriteKey {
parseAndSetFavoriteAttr(ctx, attrs)
Expand Down
1 change: 1 addition & 0 deletions pkg/eosclient/eosclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type EOSClient interface {
SetAttr(ctx context.Context, auth Authorization, attr *Attribute, errorIfExists, recursive bool, path string) error
UnsetAttr(ctx context.Context, auth Authorization, attr *Attribute, recursive bool, path string) error
GetAttr(ctx context.Context, auth Authorization, key, path string) (*Attribute, error)
GetAttrs(ctx context.Context, auth Authorization, path string) ([]*Attribute, error)
GetQuota(ctx context.Context, username string, rootAuth Authorization, path string) (*QuotaInfo, error)
SetQuota(ctx context.Context, rooAuth Authorization, info *SetQuotaInfo) error
Touch(ctx context.Context, auth Authorization, path string) error
Expand Down
Loading