From a9cb0e75fae4684d0194bf5b66b8caafa8798bd1 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Mon, 28 Jun 2021 23:48:43 +0200 Subject: [PATCH] add readonly interceptor --- .../read-only-storageprovider-interceptor.md | 5 + internal/grpc/interceptors/loader/loader.go | 6 +- .../grpc/interceptors/readonly/readonly.go | 168 ++++++++++++++++++ .../http/services/owncloud/ocdav/error.go | 47 ++++- 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 changelog/unreleased/read-only-storageprovider-interceptor.md create mode 100644 internal/grpc/interceptors/readonly/readonly.go 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/error.go b/internal/http/services/owncloud/ocdav/error.go index db1ada84465..b99a266efda 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") @@ -94,12 +101,39 @@ func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status case rpc.Code_CODE_NOT_FOUND: log.Debug().Interface("status", s).Msg("resource not found") w.WriteHeader(http.StatusNotFound) + b, err := createWebDavError(SabredavPermissionDenied, s.Message, "") + if err != nil { + log.Error().Interface("status", s).Msg("marshaling xml response failed") + w.WriteHeader(http.StatusInternalServerError) + } + _, e := w.Write(b) + if e != nil { + log.Error().Interface("status", s).Msg("failed write response") + } case rpc.Code_CODE_PERMISSION_DENIED: log.Debug().Interface("status", s).Msg("permission denied") w.WriteHeader(http.StatusForbidden) + b, err := createWebDavError(SabredavPermissionDenied, s.Message, "") + if err != nil { + log.Error().Interface("status", s).Msg("marshaling xml response failed") + w.WriteHeader(http.StatusInternalServerError) + } + _, e := w.Write(b) + if e != nil { + log.Error().Interface("status", s).Msg("failed write response") + } case rpc.Code_CODE_INVALID_ARGUMENT: log.Debug().Interface("status", s).Msg("bad request") w.WriteHeader(http.StatusBadRequest) + b, err := createWebDavError(SabredavBadRequest, s.Message, "") + if err != nil { + log.Error().Interface("status", s).Msg("marshaling xml response failed") + w.WriteHeader(http.StatusInternalServerError) + } + _, e := w.Write(b) + if e != nil { + log.Error().Interface("status", s).Msg("failed write response") + } case rpc.Code_CODE_UNIMPLEMENTED: log.Debug().Interface("status", s).Msg("not implemented") w.WriteHeader(http.StatusNotImplemented) @@ -111,3 +145,14 @@ func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status w.WriteHeader(http.StatusInternalServerError) } } + +func createWebDavError(c code, m string, h string) ([]byte, error) { + body, err := Marshal( + exception{ + code: c, + message: m, + header: h, + }, + ) + return body, err +}