diff --git a/changelog/unreleased/read-only-storageprovider-interceptor.md b/changelog/unreleased/read-only-storageprovider-interceptor.md new file mode 100644 index 00000000000..bd728f7ac31 --- /dev/null +++ b/changelog/unreleased/read-only-storageprovider-interceptor.md @@ -0,0 +1,5 @@ +Enhancement: Add readonly interceptor + +The readonly interceptor could be used to configure a storageprovider in readonly mode. This could be handy in some migration scenarios. + +https://github.com/cs3org/reva/pull/1849 diff --git a/internal/grpc/interceptors/loader/loader.go b/internal/grpc/interceptors/loader/loader.go index b739eab64e6..c5f252e3977 100644 --- a/internal/grpc/interceptors/loader/loader.go +++ b/internal/grpc/interceptors/loader/loader.go @@ -18,4 +18,8 @@ package loader -// Add your own. +import ( + // Load core GRPC services + _ "github.com/cs3org/reva/internal/grpc/interceptors/readonly" + // Add your own service here +) diff --git a/internal/grpc/interceptors/readonly/readonly.go b/internal/grpc/interceptors/readonly/readonly.go new file mode 100644 index 00000000000..e568ffc108d --- /dev/null +++ b/internal/grpc/interceptors/readonly/readonly.go @@ -0,0 +1,168 @@ +// 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 readonly + +import ( + "context" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rgrpc" + rstatus "github.com/cs3org/reva/pkg/rgrpc/status" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + defaultPriority = 200 +) + +func init() { + rgrpc.RegisterUnaryInterceptor("readonly", NewUnary) +} + +// NewUnary returns a new unary interceptor +// that checks grpc calls and blocks write requests. +func NewUnary(map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + log := appctx.GetLogger(ctx) + + switch req.(type) { + // handle known non-write request types + case *provider.CreateHomeRequest, + *provider.GetHomeRequest, + *provider.GetPathRequest, + *provider.GetQuotaRequest, + *registry.GetStorageProvidersRequest, + *provider.InitiateFileDownloadRequest, + *provider.ListFileVersionsRequest, + *provider.ListGrantsRequest, + *provider.ListRecycleRequest: + return handler(ctx, req) + case *provider.ListContainerRequest: + resp, err := handler(ctx, req) + if listResp, ok := resp.(*provider.ListContainerResponse); ok && listResp.Infos != nil { + for _, info := range listResp.Infos { + // use the existing PermissionsSet and change the writes to false + if info.PermissionSet != nil { + info.PermissionSet.AddGrant = false + info.PermissionSet.CreateContainer = false + info.PermissionSet.Delete = false + info.PermissionSet.InitiateFileUpload = false + info.PermissionSet.Move = false + info.PermissionSet.RemoveGrant = false + info.PermissionSet.PurgeRecycle = false + info.PermissionSet.RestoreFileVersion = false + info.PermissionSet.RestoreRecycleItem = false + info.PermissionSet.UpdateGrant = false + } + } + } + return resp, err + case *provider.StatRequest: + resp, err := handler(ctx, req) + if statResp, ok := resp.(*provider.StatResponse); ok && statResp.Info != nil && statResp.Info.PermissionSet != nil { + // use the existing PermissionsSet and change the writes to false + statResp.Info.PermissionSet.AddGrant = false + statResp.Info.PermissionSet.CreateContainer = false + statResp.Info.PermissionSet.Delete = false + statResp.Info.PermissionSet.InitiateFileUpload = false + statResp.Info.PermissionSet.Move = false + statResp.Info.PermissionSet.RemoveGrant = false + statResp.Info.PermissionSet.PurgeRecycle = false + statResp.Info.PermissionSet.RestoreFileVersion = false + statResp.Info.PermissionSet.RestoreRecycleItem = false + statResp.Info.PermissionSet.UpdateGrant = false + } + return resp, err + // Don't allow the following requests types + case *provider.AddGrantRequest: + return &provider.AddGrantResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to add grant on readonly storage"), + }, nil + case *provider.CreateContainerRequest: + return &provider.CreateContainerResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create resoure on readonly storage"), + }, nil + case *provider.DeleteRequest: + return &provider.DeleteResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to delete resource on readonly storage"), + }, nil + case *provider.InitiateFileUploadRequest: + return &provider.InitiateFileUploadResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to upload resource on readonly storage"), + }, nil + case *provider.MoveRequest: + return &provider.MoveResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to move resource on readonly storage"), + }, nil + case *provider.PurgeRecycleRequest: + return &provider.PurgeRecycleResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to purge recycle on readonly storage"), + }, nil + case *provider.RemoveGrantRequest: + return &provider.RemoveGrantResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to remove grant on readonly storage"), + }, nil + case *provider.RestoreRecycleItemRequest: + return &provider.RestoreRecycleItemResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to restore recycle item on readonly storage"), + }, nil + case *provider.SetArbitraryMetadataRequest: + return &provider.SetArbitraryMetadataResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to set arbitrary metadata on readonly storage"), + }, nil + case *provider.UnsetArbitraryMetadataRequest: + return &provider.UnsetArbitraryMetadataResponse{ + Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to unset arbitrary metadata on readonly storage"), + }, nil + // block unknown request types and return error + default: + log.Debug().Msg("storage is readonly") + return nil, status.Errorf(codes.PermissionDenied, "permission denied: tried to execute an unknown operation: %T!", req) + } + }, defaultPriority, nil +} + +// NewStream returns a new server stream interceptor +// that checks grpc calls and blocks write requests. +func NewStream() grpc.StreamServerInterceptor { + interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + ctx := ss.Context() + + 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/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index a3cbee49ec2..925f050f8e4 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -91,6 +91,15 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { } if srcStatRes.Status.Code != rpc.Code_CODE_OK { + if srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND{ + w.WriteHeader(http.StatusNotFound) + m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavNotFound, + message: m, + }) + HandleWebdavError(&sublog, w, b, err) + } HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -143,7 +152,7 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { // TODO what if intermediate is a file? } - err = s.descend(ctx, client, srcStatRes.Info, dst, depth == "infinity") + err = s.descend(ctx, w, client, srcStatRes.Info, dst, depth == "infinity") if err != nil { sublog.Error().Err(err).Str("depth", depth).Msg("error descending directory") w.WriteHeader(http.StatusInternalServerError) @@ -152,7 +161,7 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { w.WriteHeader(successCode) } -func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst string, recurse bool) error { +func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst string, recurse bool) error { log := appctx.GetLogger(ctx) log.Debug().Str("src", src.Path).Str("dst", dst).Msg("descending") if src.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { @@ -161,8 +170,22 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src Ref: &provider.Reference{Path: dst}, } createRes, err := client.CreateContainer(ctx, createReq) - if err != nil || createRes.Status.Code != rpc.Code_CODE_OK { - return err + if err != nil { + log.Error().Err(err).Msg("error performing create container grpc request") + w.WriteHeader(http.StatusInternalServerError) + } + + if createRes.Status.Code != rpc.Code_CODE_OK { + if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(log, w, b, err) + } + HandleErrorStatus(log, w, createRes.Status) } // TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2 @@ -185,7 +208,7 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src for i := range res.Infos { childDst := path.Join(dst, path.Base(res.Infos[i].Path)) - err := s.descend(ctx, client, res.Infos[i], childDst, recurse) + err := s.descend(ctx, w, client, res.Infos[i], childDst, recurse) if err != nil { return err } @@ -237,7 +260,16 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src } if uRes.Status.Code != rpc.Code_CODE_OK { - return fmt.Errorf("status code %d", uRes.Status.Code) + if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + m := fmt.Sprintf("Permission denied to create %v", uReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(log, w, b, err) + } + HandleErrorStatus(log, w, uRes.Status) } var uploadEP, uploadToken string diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index 8898a95d1a1..29ff9de84dd 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -19,6 +19,7 @@ package ocdav import ( + "fmt" "net/http" "path" @@ -54,6 +55,32 @@ func (s *svc) handleDelete(w http.ResponseWriter, r *http.Request, ns string) { } if res.Status.Code != rpc.Code_CODE_OK { + if res.Status.Code == rpc.Code_CODE_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + m := fmt.Sprintf("Resource %v not found", fn) + b, err := Marshal(exception{ + code: SabredavNotFound, + message: m, + }) + HandleWebdavError(&sublog, w, b, err) + } + if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + m := fmt.Sprintf("Permission denied to delete %v", fn) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(&sublog, w, b, err) + } + if res.Status.Code == rpc.Code_CODE_INTERNAL && res.Status.Message == "can't delete mount path" { + w.WriteHeader(http.StatusForbidden) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: res.Status.Message, + }) + HandleWebdavError(&sublog, w, b, err) + } HandleErrorStatus(&sublog, w, res.Status) return } diff --git a/internal/http/services/owncloud/ocdav/error.go b/internal/http/services/owncloud/ocdav/error.go index db1ada84465..725be4fb1ed 100644 --- a/internal/http/services/owncloud/ocdav/error.go +++ b/internal/http/services/owncloud/ocdav/error.go @@ -39,6 +39,10 @@ const ( SabredavNotAuthenticated // SabredavPreconditionFailed maps to HTTP 412 SabredavPreconditionFailed + // SabredavPermissionDenied maps to HTTP 403 + SabredavPermissionDenied + // SabredavNotFound maps to HTTP 404 + SabredavNotFound ) var ( @@ -47,6 +51,8 @@ var ( "Sabre\\DAV\\Exception\\MethodNotAllowed", "Sabre\\DAV\\Exception\\NotAuthenticated", "Sabre\\DAV\\Exception\\PreconditionFailed", + "Sabre\\DAV\\Exception\\PermissionDenied", + "Sabre\\DAV\\Exception\\NotFound", } ) @@ -79,7 +85,8 @@ type errorXML struct { Exception string `xml:"s:exception"` Message string `xml:"s:message"` InnerXML []byte `xml:",innerxml"` - Header string `xml:"s:header"` + // Header is used to indicate the conflicting request header + Header string `xml:"s:header,omitempty"` } var errInvalidPropfind = errors.New("webdav: invalid propfind") @@ -111,3 +118,17 @@ func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status w.WriteHeader(http.StatusInternalServerError) } } + +// HandleWebdavError checks the status code, logs an error and creates a webdav response body +// if needed +func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) { + if err != nil { + log.Error().Msgf("error marshaling xml response: %s", b) + w.WriteHeader(http.StatusInternalServerError) + return + } + _, err = w.Write(b) + if err != nil { + log.Err(err).Msg("error writing response") + } +} diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index e92bc1bedee..0cb677dfc72 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -19,6 +19,7 @@ package ocdav import ( + "fmt" "net/http" "path" "strings" @@ -76,6 +77,15 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { return } if srcStatRes.Status.Code != rpc.Code_CODE_OK { + if srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavNotFound, + message: m, + }) + HandleWebdavError(&sublog, w, b, err) + } HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -152,6 +162,15 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { } if mRes.Status.Code != rpc.Code_CODE_OK { + if mRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + m := fmt.Sprintf("Permission denied to move %v", sourceRef.Path) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(&sublog, w, b, err) + } HandleErrorStatus(&sublog, w, mRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index 18706b4519b..f767adfa8eb 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -248,6 +248,14 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } if uRes.Status.Code != rpc.Code_CODE_OK { + if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: "permission denied: you have no permission to upload content", + }) + HandleWebdavError(&sublog, w, b, err) + } HandleErrorStatus(&sublog, w, uRes.Status) return } @@ -285,16 +293,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io code: SabredavBadRequest, message: "The computed checksum does not match the one received from the client.", }) - if err != nil { - sublog.Error().Msgf("error marshaling xml response: %s", b) - w.WriteHeader(http.StatusInternalServerError) - return - } - _, err = w.Write(b) - if err != nil { - sublog.Err(err).Msg("error writing response") - } - return + HandleWebdavError(&sublog, w, b, err) } sublog.Error().Err(err).Msg("PUT request to data server failed") w.WriteHeader(httpRes.StatusCode)