From 7aabd53db2e2931da91632eb67e37a01995ef460 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte <39946305+gmgigi96@users.noreply.github.com> Date: Wed, 17 Nov 2021 14:27:44 +0100 Subject: [PATCH] Filter root path according to the agent that makes the request (#2212) --- changelog/unreleased/clean-root-path.md | 3 + internal/grpc/interceptors/log/log.go | 25 ++-- .../grpc/interceptors/useragent/useragent.go | 70 ++++++++++ internal/grpc/services/gateway/gateway.go | 1 + .../grpc/services/gateway/storageprovider.go | 42 +++++- internal/http/interceptors/auth/auth.go | 2 + pkg/ctx/agentctx.go | 26 +++- pkg/rgrpc/rgrpc.go | 3 + pkg/storage/registry/static/static.go | 44 +------ pkg/useragent/useragent.go | 77 +++++++++++ pkg/useragent/useragent_test.go | 122 ++++++++++++++++++ 11 files changed, 346 insertions(+), 69 deletions(-) create mode 100644 changelog/unreleased/clean-root-path.md create mode 100644 internal/grpc/interceptors/useragent/useragent.go create mode 100644 pkg/useragent/useragent.go create mode 100644 pkg/useragent/useragent_test.go diff --git a/changelog/unreleased/clean-root-path.md b/changelog/unreleased/clean-root-path.md new file mode 100644 index 0000000000..56126eeda8 --- /dev/null +++ b/changelog/unreleased/clean-root-path.md @@ -0,0 +1,3 @@ +Enhancement: Filter root path according to the agent that makes the request + +https://github.com/cs3org/reva/pull/2212 diff --git a/internal/grpc/interceptors/log/log.go b/internal/grpc/interceptors/log/log.go index b288d3f0c7..ded8982f9f 100644 --- a/internal/grpc/interceptors/log/log.go +++ b/internal/grpc/interceptors/log/log.go @@ -23,10 +23,10 @@ import ( "time" "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/rs/zerolog" "google.golang.org/grpc" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) @@ -40,16 +40,13 @@ func NewUnary() grpc.UnaryServerInterceptor { code := status.Code(err) end := time.Now() diff := end.Sub(start).Nanoseconds() - var fromAddress, userAgent string + var fromAddress string if p, ok := peer.FromContext(ctx); ok { fromAddress = p.Addr.Network() + "://" + p.Addr.String() } - if md, ok := metadata.FromIncomingContext(ctx); ok { - if vals, ok := md["user-agent"]; ok { - if len(vals) > 0 && vals[0] != "" { - userAgent = vals[0] - } - } + userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx) + if !ok { + userAgent = "" } log := appctx.GetLogger(ctx) @@ -77,21 +74,19 @@ func NewUnary() grpc.UnaryServerInterceptor { // that adds trace information to the request. func NewStream() grpc.StreamServerInterceptor { interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx := ss.Context() start := time.Now() err := handler(srv, ss) end := time.Now() code := status.Code(err) diff := end.Sub(start).Nanoseconds() - var fromAddress, userAgent string + var fromAddress string if p, ok := peer.FromContext(ss.Context()); ok { fromAddress = p.Addr.Network() + "://" + p.Addr.String() } - if md, ok := metadata.FromIncomingContext(ss.Context()); ok { - if vals, ok := md["user-agent"]; ok { - if len(vals) > 0 && vals[0] != "" { - userAgent = vals[0] - } - } + userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx) + if !ok { + userAgent = "" } log := appctx.GetLogger(ss.Context()) diff --git a/internal/grpc/interceptors/useragent/useragent.go b/internal/grpc/interceptors/useragent/useragent.go new file mode 100644 index 0000000000..d39642e59e --- /dev/null +++ b/internal/grpc/interceptors/useragent/useragent.go @@ -0,0 +1,70 @@ +// 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]) + } + } + wrapped := newWrappedServerStream(ctx, ss) + return handler(srv, wrapped) + } + return interceptor +} + +func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream { + return &wrappedServerStream{ServerStream: ss, newCtx: ctx} +} + +type wrappedServerStream struct { + grpc.ServerStream + newCtx context.Context +} + +func (ss *wrappedServerStream) Context() context.Context { + return ss.newCtx +} diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index 0de4137d89..3a8a6203a5 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"` + AllowedUserAgents map[string][]string `mapstructure:"allowed_user_agents"` // map[path][]user-agent CreateHomeCacheTTL int `mapstructure:"create_home_cache_ttl"` } diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 27959e8acf..66599a324e 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -35,7 +35,10 @@ 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/useragent" + ua "github.com/mileusna/useragent" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" @@ -1676,6 +1679,30 @@ func (s *svc) listSharesFolder(ctx context.Context) (*provider.ListContainerResp return lcr, nil } +func (s *svc) isPathAllowed(ua *ua.UserAgent, path string) bool { + uaLst, ok := s.c.AllowedUserAgents[path] + if !ok { + // if no user agent is defined for a path, all user agents are allowed + return true + } + return useragent.IsUserAgentAllowed(ua, uaLst) +} + +func (s *svc) filterProvidersByUserAgent(ctx context.Context, providers []*registry.ProviderInfo) []*registry.ProviderInfo { + ua, ok := ctxpkg.ContextGetUserAgent(ctx) + if !ok { + return providers + } + + filters := []*registry.ProviderInfo{} + for _, p := range providers { + if s.isPathAllowed(ua, 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 { @@ -1686,6 +1713,7 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ providers = getUniqueProviders(providers) resPath := req.Ref.GetPath() + if len(providers) == 1 && (utils.IsRelativeReference(req.Ref) || resPath == "" || strings.HasPrefix(resPath, providers[0].ProviderPath)) { c, err := s.getStorageProviderClient(ctx, providers[0]) if err != nil { @@ -1707,7 +1735,7 @@ func (s *svc) listContainerAcrossProviders(ctx context.Context, req *provider.Li nestedInfos := make(map[string]*provider.ResourceInfo) log := appctx.GetLogger(ctx) - for _, p := range providers { + for _, p := range s.filterProvidersByUserAgent(ctx, providers) { c, err := s.getStorageProviderClient(ctx, p) if err != nil { log.Err(err).Msg("error connecting to storage provider=" + p.Address) @@ -2201,15 +2229,15 @@ func (s *svc) findProviders(ctx context.Context, ref *provider.Reference) ([]*re } func getUniqueProviders(providers []*registry.ProviderInfo) []*registry.ProviderInfo { - unique := make(map[string]bool) + unique := make(map[string]*registry.ProviderInfo) for _, p := range providers { - unique[p.Address] = true + unique[p.Address] = p } - p := make([]*registry.ProviderInfo, 0, len(unique)) - for addr := range unique { - p = append(p, ®istry.ProviderInfo{Address: addr}) + res := make([]*registry.ProviderInfo, 0, len(unique)) + for _, provider := range unique { + res = append(res, provider) } - return p + return res } type etagWithTS struct { diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 2e612f032f..42988a0973 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/pkg/ctx/agentctx.go b/pkg/ctx/agentctx.go index 6b361676e1..74eda0b563 100644 --- a/pkg/ctx/agentctx.go +++ b/pkg/ctx/agentctx.go @@ -25,20 +25,34 @@ import ( "google.golang.org/grpc/metadata" ) +// UserAgentHeader is the header used for the user agent +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) { + if userAgentStr, ok := ContextGetUserAgentString(ctx); ok { + userAgent := ua.Parse(userAgentStr) + return &userAgent, true + } + return nil, false +} + +// ContextGetUserAgentString returns the user agent string if set in the given context. +func ContextGetUserAgentString(ctx context.Context) (string, bool) { md, ok := metadata.FromIncomingContext(ctx) if !ok { - return nil, false + return "", false } - userAgentLst, ok := md["user-agent"] + userAgentLst, ok := md[UserAgentHeader] if !ok { - return nil, false + userAgentLst, ok = md["user-agent"] + if !ok { + return "", false + } } if len(userAgentLst) == 0 { - return nil, false + return "", false } - userAgent := ua.Parse(userAgentLst[0]) - return &userAgent, true + return userAgentLst[0], true } diff --git a/pkg/rgrpc/rgrpc.go b/pkg/rgrpc/rgrpc.go index 220fa50199..05fa952153 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" @@ -290,6 +291,7 @@ func (s *Server) getInterceptors(unprotected []string) ([]grpc.ServerOption, err unaryInterceptors = append([]grpc.UnaryServerInterceptor{ appctx.NewUnary(s.log), token.NewUnary(), + useragent.NewUnary(), log.NewUnary(), recovery.NewUnary(), }, unaryInterceptors...) @@ -331,6 +333,7 @@ func (s *Server) getInterceptors(unprotected []string) ([]grpc.ServerOption, err authStream, appctx.NewStream(s.log), token.NewStream(), + useragent.NewStream(), log.NewStream(), recovery.NewStream(), }, streamInterceptors...) diff --git a/pkg/storage/registry/static/static.go b/pkg/storage/registry/static/static.go index d5d84403e4..fc0d2f99c7 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" - ua "github.com/mileusna/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 { @@ -140,27 +138,6 @@ func (b *reg) GetHome(ctx context.Context) (*registrypb.ProviderInfo, error) { return nil, errors.New("static: home not found") } -func userAgentIsAllowed(ua *ua.UserAgent, userAgents []string) bool { - for _, userAgent := range userAgents { - switch userAgent { - case "web": - if ua.IsChrome() || ua.IsEdge() || ua.IsFirefox() || ua.IsSafari() || - ua.IsInternetExplorer() || ua.IsOpera() || ua.IsOperaMini() { - return true - } - case "desktop": - if ua.Desktop { - return true - } - case "grpc": - if strings.HasPrefix(ua.Name, "grpc") { - return true - } - } - } - return false -} - func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*registrypb.ProviderInfo, error) { // find longest match var match *registrypb.ProviderInfo @@ -197,21 +174,6 @@ func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*re if fn != "" { for prefix, rule := range b.c.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 { - ua, ok := ctxpkg.ContextGetUserAgent(ctx) - if !ok { - continue - } - if !userAgentIsAllowed(ua, rule.AllowedUserAgents) { - continue // skip this provider - } - } - addr := getProviderAddr(ctx, rule) r, err := regexp.Compile("^" + prefix) if err != nil { diff --git a/pkg/useragent/useragent.go b/pkg/useragent/useragent.go new file mode 100644 index 0000000000..53d766ed26 --- /dev/null +++ b/pkg/useragent/useragent.go @@ -0,0 +1,77 @@ +// 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 ( + "strings" + + ua "github.com/mileusna/useragent" +) + +// isWeb returns true if the useragent is generated by the web +func isWeb(ua *ua.UserAgent) bool { + return ua.IsChrome() || ua.IsEdge() || ua.IsFirefox() || ua.IsSafari() || + ua.IsInternetExplorer() || ua.IsOpera() || ua.IsOperaMini() +} + +// isMobile returns true if the useragent is generated by the mobile +func isMobile(ua *ua.UserAgent) bool { + // workaround as the library does not recognise iOS string inside the user agent + isIOS := ua.IsIOS() || strings.Contains(ua.String, "iOS") + return !isWeb(ua) && (ua.IsAndroid() || isIOS) +} + +// isDesktop returns true if the useragent is generated by a desktop application +func isDesktop(ua *ua.UserAgent) bool { + return ua.Desktop && !isWeb(ua) +} + +// isGRPC returns true if the useragent is generated by a grpc client +func isGRPC(ua *ua.UserAgent) bool { + return strings.HasPrefix(ua.Name, "grpc") +} + +// getCategory returns the category of the user agent +// (i.e. if it is a web, mobile, desktop or grpc user agent) +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 "" + } +} + +// IsUserAgentAllowed return true if the user agent corresponds +// to one in the allowed user agents list +func IsUserAgentAllowed(ua *ua.UserAgent, allowedUserAgents []string) bool { + cat := getCategory(ua) + for _, userAgent := range allowedUserAgents { + if userAgent == cat { + return true + } + } + return false +} diff --git a/pkg/useragent/useragent_test.go b/pkg/useragent/useragent_test.go new file mode 100644 index 0000000000..c37ae97508 --- /dev/null +++ b/pkg/useragent/useragent_test.go @@ -0,0 +1,122 @@ +// 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 ( + "testing" + + ua "github.com/mileusna/useragent" +) + +func TestUserAgentIsAllowed(t *testing.T) { + + tests := []struct { + description string + userAgent string + userAgents []string + expected bool + }{ + { + description: "grpc-go", + userAgent: "grpc-go", + userAgents: []string{"grpc"}, + expected: true, + }, + { + description: "grpc-go", + userAgent: "grpc-go", + userAgents: []string{"desktop", "mobile", "web"}, + expected: false, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + 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, + }, + { + description: "mobile-android", + userAgent: "Mozilla/5.0 (Android) ownCloud-android/2.13.1 cernbox/Android", + userAgents: []string{"mobile"}, + expected: true, + }, + { + description: "mobile-ios", + userAgent: "Mozilla/5.0 (iOS) ownCloud-iOS/3.8.0 cernbox/iOS", + userAgents: []string{"mobile"}, + expected: true, + }, + { + description: "mobile-android", + userAgent: "Mozilla/5.0 (Android) ownCloud-android/2.13.1 cernbox/Android", + userAgents: []string{"web", "desktop", "grpc"}, + expected: false, + }, + { + description: "mobile-ios", + userAgent: "Mozilla/5.0 (iOS) ownCloud-iOS/3.8.0 cernbox/iOS", + userAgents: []string{"web", "desktop", "grpc"}, + expected: false, + }, + { + description: "mobile-web", + userAgent: "Mozilla/5.0 (Android 11; Mobile; rv:86.0) Gecko/86.0 Firefox/86.0", + userAgents: []string{"web"}, + expected: true, + }, + { + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + + ua := ua.Parse(tt.userAgent) + + res := IsUserAgentAllowed(&ua, tt.userAgents) + + if res != tt.expected { + t.Fatalf("result does not match with expected. got=%+v expected=%+v", res, tt.expected) + } + + }) + } +}