diff --git a/internal/grpc/interceptors/useragent/useragent.go b/internal/grpc/interceptors/useragent/useragent.go new file mode 100644 index 00000000000..043b6a6094a --- /dev/null +++ b/internal/grpc/interceptors/useragent/useragent.go @@ -0,0 +1,56 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package useragent + +import ( + "context" + + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// NewUnary returns a new unary interceptor that adds +// the useragent to the context. +func NewUnary() grpc.UnaryServerInterceptor { + interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if md, ok := metadata.FromIncomingContext(ctx); ok { + if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 { + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0]) + } + } + return handler(ctx, req) + } + return interceptor +} + +// NewStream returns a new server stream interceptor +// that adds the user agent to the context. +func NewStream() grpc.StreamServerInterceptor { + interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx := ss.Context() + if md, ok := metadata.FromIncomingContext(ctx); ok { + if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 { + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0]) + } + } + return handler(srv, ss) + } + return interceptor +} diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index 6908174e166..f4ec3227cec 100644 --- a/internal/grpc/services/gateway/gateway.go +++ b/internal/grpc/services/gateway/gateway.go @@ -69,6 +69,7 @@ type config struct { HomeMapping string `mapstructure:"home_mapping"` TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` EtagCacheTTL int `mapstructure:"etag_cache_ttl"` + HiddenRootFolders map[string][]string `mapstructure:"hidden_root_folders"` } // sets defaults @@ -116,10 +117,11 @@ func (c *config) init() { } type svc struct { - c *config - dataGatewayURL url.URL - tokenmgr token.Manager - etagCache *ttlcache.Cache `mapstructure:"etag_cache"` + c *config + dataGatewayURL url.URL + tokenmgr token.Manager + etagCache *ttlcache.Cache `mapstructure:"etag_cache"` + hiddenRootFolders map[string]map[string]struct{} } // New creates a new gateway svc that acts as a proxy for any grpc operation. @@ -148,11 +150,23 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { _ = etagCache.SetTTL(time.Duration(c.EtagCacheTTL) * time.Second) etagCache.SkipTTLExtensionOnHit(true) + // create set of hidden root folders + hiddenRootFolders := make(map[string]map[string]struct{}) + for agent, folders := range c.HiddenRootFolders { + if _, ok := hiddenRootFolders[agent]; !ok { + hiddenRootFolders[agent] = make(map[string]struct{}) + } + for _, f := range folders { + hiddenRootFolders[agent][f] = struct{}{} + } + } + s := &svc{ - c: c, - dataGatewayURL: *u, - tokenmgr: tokenManager, - etagCache: etagCache, + c: c, + dataGatewayURL: *u, + tokenmgr: tokenManager, + etagCache: etagCache, + hiddenRootFolders: hiddenRootFolders, } return s, nil diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index dcfd7be3e76..7a22ea7b714 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -35,6 +35,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + ctxpkg "github.com/cs3org/reva/pkg/ctx" rtrace "github.com/cs3org/reva/pkg/trace" "github.com/cs3org/reva/pkg/appctx" @@ -42,6 +43,7 @@ import ( "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/storage/utils/etag" + "github.com/cs3org/reva/pkg/useragent" "github.com/cs3org/reva/pkg/utils" "github.com/golang-jwt/jwt" "github.com/google/uuid" @@ -1648,6 +1650,31 @@ func (s *svc) listSharesFolder(ctx context.Context) (*provider.ListContainerResp return lcr, nil } +func (s *svc) isFolderHidden(ua, path string) bool { + uaSet, ok := s.hiddenRootFolders[ua] + if !ok { + return false + } + _, ok = uaSet[path] + return ok +} + +func (s *svc) filterProvidersByUserAgent(ctx context.Context, providers []*registry.ProviderInfo) []*registry.ProviderInfo { + ua, ok := ctxpkg.ContextGetUserAgent(ctx) + if !ok { + return providers + } + cat := useragent.GetCategory(ua) + + filters := []*registry.ProviderInfo{} + for _, p := range providers { + if !s.isFolderHidden(cat, p.ProviderPath) { + filters = append(filters, p) + } + } + return filters +} + func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { providers, err := s.findProviders(ctx, req.Ref) if err != nil { @@ -1672,6 +1699,18 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ return rsp, nil } + p, st := s.getPath(ctx, req.Ref, req.ArbitraryMetadataKeys...) + if st.Code != rpc.Code_CODE_OK { + return &provider.ListContainerResponse{ + Status: st, + }, nil + } + + // check if the path is the root folder + if path.Clean(p) == "/" { + providers = s.filterProvidersByUserAgent(ctx, providers) + } + return s.listContainerAcrossProviders(ctx, req, providers) } diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 2e612f032f4..42988a0973a 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -281,6 +281,8 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err ctx = ctxpkg.ContextSetToken(ctx, tkn) ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials? + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) + r = r.WithContext(ctx) h.ServeHTTP(w, r) }) diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 90600e393ed..38ddb1df9b8 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -167,6 +167,7 @@ func (s *svc) getResourceInfos(ctx context.Context, w http.ResponseWriter, r *ht if depth == "" { depth = "1" } + log.Error().Msgf("***** getResourceInfos depth=%s *****", depth) // see https://tools.ietf.org/html/rfc4918#section-9.1 if depth != "0" && depth != "1" && depth != "infinity" { log.Debug().Str("depth", depth).Msgf("invalid Depth header value") @@ -259,6 +260,8 @@ func (s *svc) getResourceInfos(ctx context.Context, w http.ResponseWriter, r *ht Ref: ref, ArbitraryMetadataKeys: metadataKeys, } + ua, _ := ctxpkg.ContextGetUserAgent(ctx) + log.Error().Msgf("***** calling client.ListContainer - useragent = %+v *****", ua) res, err := client.ListContainer(ctx, req) if err != nil { log.Error().Err(err).Msg("error sending list container grpc request") diff --git a/pkg/ctx/agentctx.go b/pkg/ctx/agentctx.go index 6b361676e18..db7fba3495b 100644 --- a/pkg/ctx/agentctx.go +++ b/pkg/ctx/agentctx.go @@ -25,6 +25,8 @@ import ( "google.golang.org/grpc/metadata" ) +const UserAgentHeader = "x-user-agent" + // ContextGetUserAgent returns the user agent if set in the given context. // see https://github.com/grpc/grpc-go/issues/1100 func ContextGetUserAgent(ctx context.Context) (*ua.UserAgent, bool) { @@ -32,9 +34,12 @@ func ContextGetUserAgent(ctx context.Context) (*ua.UserAgent, bool) { if !ok { return nil, false } - userAgentLst, ok := md["user-agent"] + userAgentLst, ok := md[UserAgentHeader] if !ok { - return nil, false + userAgentLst, ok = md["user-agent"] + if !ok { + return nil, false + } } if len(userAgentLst) == 0 { return nil, false diff --git a/pkg/rgrpc/rgrpc.go b/pkg/rgrpc/rgrpc.go index 220fa50199e..7317cd00bb3 100644 --- a/pkg/rgrpc/rgrpc.go +++ b/pkg/rgrpc/rgrpc.go @@ -29,6 +29,7 @@ import ( "github.com/cs3org/reva/internal/grpc/interceptors/log" "github.com/cs3org/reva/internal/grpc/interceptors/recovery" "github.com/cs3org/reva/internal/grpc/interceptors/token" + "github.com/cs3org/reva/internal/grpc/interceptors/useragent" "github.com/cs3org/reva/pkg/sharedconf" rtrace "github.com/cs3org/reva/pkg/trace" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" @@ -292,6 +293,7 @@ func (s *Server) getInterceptors(unprotected []string) ([]grpc.ServerOption, err token.NewUnary(), log.NewUnary(), recovery.NewUnary(), + useragent.NewUnary(), }, unaryInterceptors...) unaryChain := grpc_middleware.ChainUnaryServer(unaryInterceptors...) @@ -333,6 +335,7 @@ func (s *Server) getInterceptors(unprotected []string) ([]grpc.ServerOption, err token.NewStream(), log.NewStream(), recovery.NewStream(), + useragent.NewStream(), }, streamInterceptors...) streamChain := grpc_middleware.ChainStreamServer(streamInterceptors...) diff --git a/pkg/storage/registry/static/static.go b/pkg/storage/registry/static/static.go index aeada55a32f..fc0d2f99c73 100644 --- a/pkg/storage/registry/static/static.go +++ b/pkg/storage/registry/static/static.go @@ -32,7 +32,6 @@ import ( "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/registry/registry" "github.com/cs3org/reva/pkg/storage/utils/templates" - "github.com/cs3org/reva/pkg/useragent" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" ) @@ -44,10 +43,9 @@ func init() { var bracketRegex = regexp.MustCompile(`\[(.*?)\]`) type rule struct { - Mapping string `mapstructure:"mapping"` - Address string `mapstructure:"address"` - Aliases map[string]string `mapstructure:"aliases"` - AllowedUserAgents []string `mapstructure:"allowed_user_agents"` + Mapping string `mapstructure:"mapping"` + Address string `mapstructure:"address"` + Aliases map[string]string `mapstructure:"aliases"` } type config struct { @@ -112,7 +110,7 @@ func getProviderAddr(ctx context.Context, r rule) string { func (b *reg) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) { providers := []*registrypb.ProviderInfo{} - for k, v := range rulesFilteredByUserAgent(ctx, b.c.Rules) { + for k, v := range b.c.Rules { if addr := getProviderAddr(ctx, v); addr != "" { combs := generateRegexCombinations(k) for _, c := range combs { @@ -140,24 +138,6 @@ func (b *reg) GetHome(ctx context.Context) (*registrypb.ProviderInfo, error) { return nil, errors.New("static: home not found") } -func rulesFilteredByUserAgent(ctx context.Context, rules map[string]rule) map[string]rule { - filtered := make(map[string]rule) - for prefix, rule := range rules { - // check if the provider is allowed to be shown according to the - // user agent that made the request - // if the list of AllowedUserAgents is empty, this means that - // every agents that made the request could see the provider - - if len(rule.AllowedUserAgents) != 0 { - if ua, ok := ctxpkg.ContextGetUserAgent(ctx); !ok || !useragent.IsAllowed(ua, rule.AllowedUserAgents) { - continue - } - } - filtered[prefix] = rule - } - return filtered -} - func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*registrypb.ProviderInfo, error) { // find longest match var match *registrypb.ProviderInfo @@ -166,7 +146,7 @@ func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*re // If the reference has a resource id set, use it to route if ref.ResourceId != nil { if ref.ResourceId.StorageId != "" { - for prefix, rule := range rulesFilteredByUserAgent(ctx, b.c.Rules) { + for prefix, rule := range b.c.Rules { addr := getProviderAddr(ctx, rule) r, err := regexp.Compile("^" + prefix + "$") if err != nil { @@ -192,7 +172,7 @@ func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*re // TODO this needs to be reevaluated once all clients query the storage registry for a list of storage providers fn := path.Clean(ref.GetPath()) if fn != "" { - for prefix, rule := range rulesFilteredByUserAgent(ctx, b.c.Rules) { + for prefix, rule := range b.c.Rules { addr := getProviderAddr(ctx, rule) r, err := regexp.Compile("^" + prefix) diff --git a/pkg/useragent/useragent.go b/pkg/useragent/useragent.go index 5d42ea61417..bdc30bf58c4 100644 --- a/pkg/useragent/useragent.go +++ b/pkg/useragent/useragent.go @@ -47,32 +47,17 @@ func isGRPC(ua *ua.UserAgent) bool { return strings.HasPrefix(ua.Name, "grpc") } -// IsAllowed returns true if the user agent category is one in the user agents list -func IsAllowed(ua *ua.UserAgent, userAgents []string) bool { - isWeb := isWeb(ua) - isMobile := isMobile(ua) - isDesktop := isDesktop(ua) - isGRPC := isGRPC(ua) - - for _, userAgent := range userAgents { - switch userAgent { - case "web": - if isWeb { - return true - } - case "mobile": - if isMobile { - return true - } - case "desktop": - if isDesktop { - return true - } - case "grpc": - if isGRPC { - return true - } - } +func GetCategory(ua *ua.UserAgent) string { + switch { + case isWeb(ua): + return "web" + case isMobile(ua): + return "mobile" + case isDesktop(ua): + return "desktop" + case isGRPC(ua): + return "grpc" + default: + return "" } - return false } diff --git a/pkg/useragent/useragent_test.go b/pkg/useragent/useragent_test.go index 69a1b0ea484..ac992e41109 100644 --- a/pkg/useragent/useragent_test.go +++ b/pkg/useragent/useragent_test.go @@ -29,80 +29,67 @@ func TestUserAgentIsAllowed(t *testing.T) { tests := []struct { description string userAgent string - userAgents []string - expected bool + expected string }{ { description: "grpc-go", userAgent: "grpc-go", - userAgents: []string{"grpc"}, - expected: true, + expected: "grpc", }, { description: "grpc-go", userAgent: "grpc-go", - userAgents: []string{"desktop", "mobile", "web"}, - expected: false, + expected: "grpc", }, { description: "web-firefox", userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", - userAgents: []string{"web"}, - expected: true, + expected: "web", }, { description: "web-firefox", userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15", - userAgents: []string{"desktop", "mobile", "grpc"}, - expected: false, + expected: "web", }, { description: "desktop-mirall", userAgent: "Mozilla/5.0 (Linux) mirall/2.7.1 (build 2596) (cernboxcmd, centos-3.10.0-1160.36.2.el7.x86_64 ClientArchitecture: x86_64 OsArchitecture: x86_64)", - userAgents: []string{"desktop"}, - expected: true, + expected: "desktop", }, { description: "desktop-mirall", userAgent: "Mozilla/5.0 (Linux) mirall/2.7.1 (build 2596) (cernboxcmd, centos-3.10.0-1160.36.2.el7.x86_64 ClientArchitecture: x86_64 OsArchitecture: x86_64)", - userAgents: []string{"web", "mobile", "grpc"}, - expected: false, + expected: "desktop", }, { description: "mobile-android", userAgent: "Mozilla/5.0 (Android) ownCloud-android/2.13.1 cernbox/Android", - userAgents: []string{"mobile"}, - expected: true, + expected: "mobile", }, { description: "mobile-ios", userAgent: "Mozilla/5.0 (iOS) ownCloud-iOS/3.8.0 cernbox/iOS", - userAgents: []string{"mobile"}, - expected: true, + expected: "mobile", }, { description: "mobile-android", userAgent: "Mozilla/5.0 (Android) ownCloud-android/2.13.1 cernbox/Android", - userAgents: []string{"web", "desktop", "grpc"}, - expected: false, + expected: "mobile", }, { description: "mobile-ios", userAgent: "Mozilla/5.0 (iOS) ownCloud-iOS/3.8.0 cernbox/iOS", - userAgents: []string{"web", "desktop", "grpc"}, - expected: false, + expected: "mobile", }, { description: "mobile-web", userAgent: "Mozilla/5.0 (Android 11; Mobile; rv:86.0) Gecko/86.0 Firefox/86.0", - userAgents: []string{"web"}, - expected: true, + expected: "web", }, { description: "mobile-web", userAgent: "Mozilla/5.0 (Android 11; Mobile; rv:86.0) Gecko/86.0 Firefox/86.0", - userAgents: []string{"desktop", "grpc", "mobile"}, - expected: false, + expected: "web", }, } @@ -111,7 +98,7 @@ func TestUserAgentIsAllowed(t *testing.T) { ua := ua.Parse(tt.userAgent) - res := IsAllowed(&ua, tt.userAgents) + res := GetCategory(&ua) if res != tt.expected { t.Fatalf("result does not match with expected. got=%+v expected=%+v", res, tt.expected)