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

Features for favorites xattrs in EOS, cache for scope expansion #2686

Merged
merged 3 commits into from
Mar 28, 2022
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
3 changes: 3 additions & 0 deletions changelog/unreleased/eos-scope-devs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Enhancement: Features for favorites xattrs in EOS, cache for scope expansion

https://github.com/cs3org/reva/pull/2686
26 changes: 16 additions & 10 deletions internal/grpc/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
)

var userGroupsCache gcache.Cache
var scopeExpansionCache gcache.Cache

type config struct {
// TODO(labkode): access a map is more performant as uri as fixed in length
Expand Down Expand Up @@ -75,6 +76,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
conf.GatewayAddr = sharedconf.GetGatewaySVC(conf.GatewayAddr)

userGroupsCache = gcache.New(1000000).LFU().Build()
scopeExpansionCache = gcache.New(1000000).LFU().Build()

h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
Expand All @@ -96,7 +98,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false)
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true)
if err == nil {
ctx = ctxpkg.ContextSetUser(ctx, u)
}
Expand All @@ -112,7 +114,7 @@ func NewUnary(m map[string]interface{}, unprotected []string) (grpc.UnaryServerI
}

// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, true)
u, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr, false)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return nil, status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
Expand All @@ -137,6 +139,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
}

userGroupsCache = gcache.New(1000000).LFU().Build()
scopeExpansionCache = gcache.New(1000000).LFU().Build()

h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
Expand All @@ -159,7 +162,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false)
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true)
if err == nil {
ctx = ctxpkg.ContextSetUser(ctx, u)
ss = newWrappedServerStream(ctx, ss)
Expand All @@ -177,7 +180,7 @@ func NewStream(m map[string]interface{}, unprotected []string) (grpc.StreamServe
}

// validate the token and ensure access to the resource is allowed
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, true)
u, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr, false)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
Expand All @@ -204,18 +207,21 @@ func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}

func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, fetchUserGroups bool) (*userpb.User, error) {
func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string, unprotected bool) (*userpb.User, error) {
u, tokenScope, err := mgr.DismantleToken(ctx, tkn)
if err != nil {
return nil, err
}

client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return nil, err
if unprotected {
return u, nil
}

if sharedconf.SkipUserGroupsInToken() && fetchUserGroups {
if sharedconf.SkipUserGroupsInToken() {
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return nil, err
}
groups, err := getUserGroups(ctx, u, client)
if err != nil {
return nil, err
Expand All @@ -232,7 +238,7 @@ func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.
return u, nil
}

if err = expandAndVerifyScope(ctx, req, tokenScope, gatewayAddr, mgr); err != nil {
if err = expandAndVerifyScope(ctx, req, tokenScope, u, gatewayAddr, mgr); err != nil {
return nil, err
}

Expand Down
192 changes: 112 additions & 80 deletions internal/grpc/interceptors/auth/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package auth
import (
"context"
"strings"
"time"

appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
Expand All @@ -40,10 +41,16 @@ import (
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/token"
"github.com/cs3org/reva/pkg/utils"
"github.com/cs3org/reva/pkg/utils/resourceid"
"google.golang.org/grpc/metadata"
)

func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, gatewayAddr string, mgr token.Manager) error {
const (
scopeDelimiter = "#"
scopeCacheExpiration = 3600
)

func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, user *userpb.User, gatewayAddr string, mgr token.Manager) error {
log := appctx.GetLogger(ctx)
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
Expand All @@ -52,108 +59,63 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s

hasEditorRole := false
for _, v := range tokenScope {
if v.Role == authpb.Role_ROLE_EDITOR {
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR {
hasEditorRole = true
break
}
}

if ref, ok := extractRef(req, hasEditorRole); ok {
// Check if req is of type *provider.Reference_Path
// If yes, the request might be coming from a share where the accessor is
// trying to impersonate the owner, since the share manager doesn't know the
// share path.
if ref.GetPath() != "" {
log.Info().Msgf("resolving path reference to ID to check token scope %+v", ref.GetPath())
for k := range tokenScope {
switch {
case strings.HasPrefix(k, "publicshare"):
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(tokenScope[k].Resource.Value, &share)
if err != nil {
continue
}
if ok, err := checkIfNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil && ok {
return nil
}
// The request is for a storage reference. This can be the case for multiple scenarios:
// - If the path is not empty, the request might be coming from a share where the accessor is
// trying to impersonate the owner, since the share manager doesn't know the
// share path.
// - If the ID not empty, the request might be coming from
// - a resource present inside a shared folder, or
// - a share created for a lightweight account after the token was minted.
log.Info().Msgf("resolving storage reference to check token scope %s", ref.String())
for k := range tokenScope {
switch {
case strings.HasPrefix(k, "publicshare"):
if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}

case strings.HasPrefix(k, "share"):
var share collaboration.Share
err := utils.UnmarshalJSONToProtoV1(tokenScope[k].Resource.Value, &share)
if err != nil {
continue
}
if ok, err := checkIfNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil && ok {
return nil
}
case strings.HasPrefix(k, "lightweight"):
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 _, share := range shares.Shares {
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
return nil
}
}
case strings.HasPrefix(k, "share"):
if err = resolveUserShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
}
} else {
// ref has ID present
// The request might be coming from
// - a resource present inside a shared folder, or
// - a share created for a lightweight account after the token was minted.

client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return err
}
for k := range tokenScope {
if strings.HasPrefix(k, "lightweight") {
log.Info().Msgf("resolving ID reference against received shares to verify token scope %+v", ref.GetResourceId())
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 _, share := range shares.Shares {
if utils.ResourceIDEqual(share.Share.ResourceId, ref.GetResourceId()) {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
return nil
}
}
} else if strings.HasPrefix(k, "publicshare") {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(tokenScope[k].Resource.Value, &share)
if err != nil {
continue
}
if ok, err := checkIfNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil && ok {
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)
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return err
}
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
}
Expand All @@ -169,6 +131,69 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s
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
}

shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
return errtypes.InternalError("error listing received shares")
}

for _, share := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + resourceid.OwnCloudResourceIDWrap(share.Share.ResourceId)
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)

if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}
}

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

func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return err
}

return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}

func resolveUserShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share collaboration.Share
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return err
}

return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}

func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, resource *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) error {
// Check if this ref is cached
key := resourceid.OwnCloudResourceIDWrap(resource) + scopeDelimiter + getRefKey(ref)
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}

if ok, err := checkIfNestedResource(ctx, ref, resource, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}

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

func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) (bool, error) {
// Since the resource ID is obtained from the scope, the current token
// has access to it.
Expand Down Expand Up @@ -270,3 +295,10 @@ func extractShareRef(req interface{}) (*collaboration.ShareReference, bool) {
}
return nil, false
}

func getRefKey(ref *provider.Reference) string {
if ref.Path != "" {
return ref.Path
}
return resourceid.OwnCloudResourceIDWrap(ref.ResourceId)
}
3 changes: 1 addition & 2 deletions internal/grpc/services/gateway/authprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)

// Commenting out as the token size can get too big
// For now, we'll try to resolve all resources on every request
// TODO(ishank011): Add a cache for these
// For now, we'll try to resolve all resources on every request and cache those
/* scope, err := s.expandScopes(ctx, res.TokenScope)
if err != nil {
err = errors.Wrap(err, "authsvc: error expanding token scope")
Expand Down
Loading