diff --git a/changelog/unreleased/decomposedfs-locking.md b/changelog/unreleased/decomposedfs-locking.md new file mode 100644 index 00000000000..3e027c95796 --- /dev/null +++ b/changelog/unreleased/decomposedfs-locking.md @@ -0,0 +1,5 @@ +Enhancement: add locking support to decomposedfs + +The decomposedfs now implements application level locking + +https://github.com/cs3org/reva/pull/2460 diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index b2337d64f1b..4c28d856d6e 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -743,6 +743,7 @@ func (s *svc) SetLock(ctx context.Context, req *provider.SetLockRequest) (*provi return nil, errors.Wrap(err, "gateway: error calling SetLock") } + s.cache.RemoveStat(ctxpkg.ContextMustGetUser(ctx), req.Ref.ResourceId) return res, nil } @@ -783,6 +784,7 @@ func (s *svc) RefreshLock(ctx context.Context, req *provider.RefreshLockRequest) return nil, errors.Wrap(err, "gateway: error calling RefreshLock") } + s.cache.RemoveStat(ctxpkg.ContextMustGetUser(ctx), req.Ref.ResourceId) return res, nil } @@ -803,6 +805,7 @@ func (s *svc) Unlock(ctx context.Context, req *provider.UnlockRequest) (*provide return nil, errors.Wrap(err, "gateway: error calling Unlock") } + s.cache.RemoveStat(ctxpkg.ContextMustGetUser(ctx), req.Ref.ResourceId) return res, nil } diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index c3e88d16d1a..2d40c80ffe5 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -66,26 +66,27 @@ func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareReq // TODO(labkode): if both commits are enabled they could be done concurrently. if s.c.CommitShareToStorageGrant { // If the share is a denial we call denyGrant instead. + var status *rpc.Status if grants.PermissionsEqual(req.Grant.Permissions.Permissions, &provider.ResourcePermissions{}) { - denyGrantStatus, err := s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee) + status, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee) if err != nil { return nil, errors.Wrap(err, "gateway: error denying grant in storage") } - if denyGrantStatus.Code != rpc.Code_CODE_OK { - return &collaboration.CreateShareResponse{ - Status: denyGrantStatus, - }, err + } else { + status, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions) + if err != nil { + return nil, errors.Wrap(err, "gateway: error adding grant to storage") } - return res, nil } - addGrantStatus, err := s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions) - if err != nil { - return nil, errors.Wrap(err, "gateway: error adding grant to storage") - } - if addGrantStatus.Code != rpc.Code_CODE_OK { + switch status.Code { + case rpc.Code_CODE_OK: + // ok + case rpc.Code_CODE_UNIMPLEMENTED: + appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring") + default: return &collaboration.CreateShareResponse{ - Status: addGrantStatus, + Status: status, }, err } } @@ -493,12 +494,7 @@ func (s *svc) denyGrant(ctx context.Context, id *provider.ResourceId, g *provide if err != nil { return nil, errors.Wrap(err, "gateway: error calling DenyGrant") } - if grantRes.Status.Code != rpc.Code_CODE_OK { - return status.NewInternal(ctx, - "error committing share to storage grant"), nil - } - - return status.NewOK(ctx), nil + return grantRes.Status, nil } func (s *svc) addGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions) (*rpc.Status, error) { @@ -530,12 +526,7 @@ func (s *svc) addGrant(ctx context.Context, id *provider.ResourceId, g *provider if err != nil { return nil, errors.Wrap(err, "gateway: error calling AddGrant") } - if grantRes.Status.Code != rpc.Code_CODE_OK { - return status.NewInternal(ctx, - "error committing share to storage grant"), nil - } - - return status.NewOK(ctx), nil + return grantRes.Status, nil } func (s *svc) updateGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions) (*rpc.Status, error) { diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 8219d2178e0..c7fd5d9a064 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -186,152 +186,70 @@ func registerMimeTypes(mimes map[string]string) { } func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest) (*provider.SetArbitraryMetadataResponse, error) { - if err := s.storage.SetArbitraryMetadata(ctx, req.Ref, req.ArbitraryMetadata); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when setting arbitrary metadata") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error setting arbitrary metadata: "+req.Ref.String()) + // FIXME these should be part of the SetArbitraryMetadataRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Msg("failed to set arbitrary metadata") - return &provider.SetArbitraryMetadataResponse{ - Status: st, - }, nil } - res := &provider.SetArbitraryMetadataResponse{ - Status: status.NewOK(ctx), - } - return res, nil + err := s.storage.SetArbitraryMetadata(ctx, req.Ref, req.ArbitraryMetadata) + + return &provider.SetArbitraryMetadataResponse{ + Status: status.NewStatusFromErrType(ctx, "set arbitrary metadata", err), + }, nil } func (s *service) UnsetArbitraryMetadata(ctx context.Context, req *provider.UnsetArbitraryMetadataRequest) (*provider.UnsetArbitraryMetadataResponse, error) { - if err := s.storage.UnsetArbitraryMetadata(ctx, req.Ref, req.ArbitraryMetadataKeys); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when unsetting arbitrary metadata") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error unsetting arbitrary metadata: "+req.Ref.String()) + // FIXME these should be part of the UnsetArbitraryMetadataRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Msg("failed to unset arbitrary metadata") - return &provider.UnsetArbitraryMetadataResponse{ - Status: st, - }, nil } - res := &provider.UnsetArbitraryMetadataResponse{ - Status: status.NewOK(ctx), - } - return res, nil + err := s.storage.UnsetArbitraryMetadata(ctx, req.Ref, req.ArbitraryMetadataKeys) + + return &provider.UnsetArbitraryMetadataResponse{ + Status: status.NewStatusFromErrType(ctx, "unset arbitrary metadata", err), + }, nil } // SetLock puts a lock on the given reference func (s *service) SetLock(ctx context.Context, req *provider.SetLockRequest) (*provider.SetLockResponse, error) { - if err := s.storage.SetLock(ctx, req.Ref, req.Lock); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when setting lock") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, fmt.Sprintf("error setting lock %s: %s", req.Ref.String(), err)) - } - return &provider.SetLockResponse{ - Status: st, - }, nil - } + err := s.storage.SetLock(ctx, req.Ref, req.Lock) - res := &provider.SetLockResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.SetLockResponse{ + Status: status.NewStatusFromErrType(ctx, "set lock", err), + }, nil } // GetLock returns an existing lock on the given reference func (s *service) GetLock(ctx context.Context, req *provider.GetLockRequest) (*provider.GetLockResponse, error) { - var lock *provider.Lock - var err error - if lock, err = s.storage.GetLock(ctx, req.Ref); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when getting lock") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, fmt.Sprintf("error getting lock %s: %s", req.Ref.String(), err)) - } - return &provider.GetLockResponse{ - Status: st, - }, nil - } + lock, err := s.storage.GetLock(ctx, req.Ref) - res := &provider.GetLockResponse{ - Status: status.NewOK(ctx), + return &provider.GetLockResponse{ + Status: status.NewStatusFromErrType(ctx, "get lock", err), Lock: lock, - } - return res, nil + }, nil } // RefreshLock refreshes an existing lock on the given reference func (s *service) RefreshLock(ctx context.Context, req *provider.RefreshLockRequest) (*provider.RefreshLockResponse, error) { - if err := s.storage.RefreshLock(ctx, req.Ref, req.Lock); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when refreshing lock") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, fmt.Sprintf("error refreshing lock %s: %s", req.Ref.String(), err)) - } - return &provider.RefreshLockResponse{ - Status: st, - }, nil - } + err := s.storage.RefreshLock(ctx, req.Ref, req.Lock) - res := &provider.RefreshLockResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.RefreshLockResponse{ + Status: status.NewStatusFromErrType(ctx, "refresh lock", err), + }, nil } // Unlock removes an existing lock from the given reference func (s *service) Unlock(ctx context.Context, req *provider.UnlockRequest) (*provider.UnlockResponse, error) { - if err := s.storage.Unlock(ctx, req.Ref); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when unlocking") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, fmt.Sprintf("error unlocking %s: %s", req.Ref.String(), err)) - } - return &provider.UnlockResponse{ - Status: st, - }, nil - } + err := s.storage.Unlock(ctx, req.Ref, req.Lock) - res := &provider.UnlockResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.UnlockResponse{ + Status: status.NewStatusFromErrType(ctx, "unlock", err), + }, nil } func (s *service) InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) { @@ -373,6 +291,13 @@ func (s *service) InitiateFileUpload(ctx context.Context, req *provider.Initiate }, nil } + // FIXME these should be part of the InitiateFileUploadRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) + } + } + metadata := map[string]string{} var uploadLength int64 if req.Opaque != nil && req.Opaque.Map != nil { @@ -603,57 +528,33 @@ func (s *service) DeleteStorageSpace(ctx context.Context, req *provider.DeleteSt } func (s *service) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { - if err := s.storage.CreateDir(ctx, req.Ref); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when creating container") - case errtypes.AlreadyExists: - st = status.NewAlreadyExists(ctx, err, "container already exists") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error creating container: "+req.Ref.String()) + // FIXME these should be part of the CreateContainerRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to create container") - return &provider.CreateContainerResponse{ - Status: st, - }, nil } - res := &provider.CreateContainerResponse{ - Status: status.NewOK(ctx), - } - return res, nil + err := s.storage.CreateDir(ctx, req.Ref) + + return &provider.CreateContainerResponse{ + Status: status.NewStatusFromErrType(ctx, "create container", err), + }, nil } func (s *service) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) { - if err := s.storage.TouchFile(ctx, req.Ref); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when touching the file") - case errtypes.AlreadyExists: - st = status.NewAlreadyExists(ctx, err, "file already exists") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error touching file: "+req.Ref.String()) + // FIXME these should be part of the TouchFileRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - return &provider.TouchFileResponse{ - Status: st, - }, nil } - res := &provider.TouchFileResponse{ - Status: status.NewOK(ctx), - } - return res, nil + err := s.storage.TouchFile(ctx, req.Ref) + + return &provider.TouchFileResponse{ + Status: status.NewStatusFromErrType(ctx, "touch file", err), + }, nil } func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { @@ -664,68 +565,36 @@ func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro } // check DeleteRequest for any known opaque properties. + // FIXME these should be part of the DeleteRequest object if req.Opaque != nil { - _, ok := req.Opaque.Map["deleting_shared_resource"] - if ok { + if _, ok := req.Opaque.Map["deleting_shared_resource"]; ok { // it is a binary key; its existence signals true. Although, do not assume. ctx = context.WithValue(ctx, appctx.DeletingSharedResource, true) } - } - - if err := s.storage.Delete(ctx, req.Ref); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when creating container") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error deleting file: "+req.Ref.String()) + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to delete") - return &provider.DeleteResponse{ - Status: st, - }, nil } - res := &provider.DeleteResponse{ - Status: status.NewOK(ctx), - } - return res, nil + err := s.storage.Delete(ctx, req.Ref) + + return &provider.DeleteResponse{ + Status: status.NewStatusFromErrType(ctx, "delete", err), + }, nil } func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { - if err := s.storage.Move(ctx, req.Source, req.Destination); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when moving") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error moving: "+req.Source.String()) + // FIXME these should be part of the MoveRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("source_reference", req.Source). - Interface("target_reference", req.Destination). - Msg("failed to move") - return &provider.MoveResponse{ - Status: st, - }, nil } + err := s.storage.Move(ctx, req.Source, req.Destination) - res := &provider.MoveResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.MoveResponse{ + Status: status.NewStatusFromErrType(ctx, "move", err), + }, nil } func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provider.StatResponse, error) { @@ -738,32 +607,11 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }) md, err := s.storage.GetMD(ctx, req.Ref, req.ArbitraryMetadataKeys) - if err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when statting") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error statting: "+req.Ref.String()) - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to stat") - return &provider.StatResponse{ - Status: st, - }, nil - } - res := &provider.StatResponse{ - Status: status.NewOK(ctx), + return &provider.StatResponse{ + Status: status.NewStatusFromErrType(ctx, "stat", err), Info: md, - } - return res, nil + }, nil } func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, ss provider.ProviderAPI_ListContainerStreamServer) error { @@ -812,29 +660,9 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { mds, err := s.storage.ListFolder(ctx, req.Ref, req.ArbitraryMetadataKeys) - if err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when listing container") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error listing container: "+req.Ref.String()) - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to list folder") - return &provider.ListContainerResponse{ - Status: st, - }, nil - } res := &provider.ListContainerResponse{ - Status: status.NewOK(ctx), + Status: status.NewStatusFromErrType(ctx, "list container", err), Infos: mds, } return res, nil @@ -842,63 +670,27 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer func (s *service) ListFileVersions(ctx context.Context, req *provider.ListFileVersionsRequest) (*provider.ListFileVersionsResponse, error) { revs, err := s.storage.ListRevisions(ctx, req.Ref) - if err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when listing file versions") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error listing file versions: "+req.Ref.String()) - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to list file versions") - return &provider.ListFileVersionsResponse{ - Status: st, - }, nil - } sort.Sort(descendingMtime(revs)) - res := &provider.ListFileVersionsResponse{ - Status: status.NewOK(ctx), + return &provider.ListFileVersionsResponse{ + Status: status.NewStatusFromErrType(ctx, "list file versions", err), Versions: revs, - } - return res, nil + }, nil } func (s *service) RestoreFileVersion(ctx context.Context, req *provider.RestoreFileVersionRequest) (*provider.RestoreFileVersionResponse, error) { - if err := s.storage.RestoreRevision(ctx, req.Ref, req.Key); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when restoring file versions") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error restoring version: "+req.Ref.String()) + // FIXME these should be part of the RestoreFileVersionRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Str("key", req.Key). - Msg("failed to restore file version") - return &provider.RestoreFileVersionResponse{ - Status: st, - }, nil } + err := s.storage.RestoreRevision(ctx, req.Ref, req.Key) - res := &provider.RestoreFileVersionResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.RestoreFileVersionResponse{ + Status: status.NewStatusFromErrType(ctx, "restore file version", err), + }, nil } func (s *service) ListRecycleStream(req *provider.ListRecycleStreamRequest, ss provider.ProviderAPI_ListRecycleStreamServer) error { @@ -980,37 +772,31 @@ func (s *service) ListRecycle(ctx context.Context, req *provider.ListRecycleRequ } func (s *service) RestoreRecycleItem(ctx context.Context, req *provider.RestoreRecycleItemRequest) (*provider.RestoreRecycleItemResponse, error) { - // TODO(labkode): CRITICAL: fill recycle info with storage provider. - key, itemPath := router.ShiftPath(req.Key) - if err := s.storage.RestoreRecycleItem(ctx, req.Ref, key, itemPath, req.RestoreRef); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when restoring recycle bin item") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error restoring recycle bin item") + // FIXME these should be part of the RestoreRecycleItemRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Str("key", req.Key). - Msg("failed to restore recycle item") - return &provider.RestoreRecycleItemResponse{ - Status: st, - }, nil } + // TODO(labkode): CRITICAL: fill recycle info with storage provider. + key, itemPath := router.ShiftPath(req.Key) + err := s.storage.RestoreRecycleItem(ctx, req.Ref, key, itemPath, req.RestoreRef) + res := &provider.RestoreRecycleItemResponse{ - Status: status.NewOK(ctx), + Status: status.NewStatusFromErrType(ctx, "restore recycle item", err), } return res, nil } func (s *service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRequest) (*provider.PurgeRecycleResponse, error) { + // FIXME these should be part of the PurgeRecycleRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) + } + } + // if a key was sent as opaque id purge only that item key, itemPath := router.ShiftPath(req.Key) if key != "" { @@ -1121,12 +907,15 @@ func (s *service) DenyGrant(ctx context.Context, req *provider.DenyGrantRequest) func (s *service) AddGrant(ctx context.Context, req *provider.AddGrantRequest) (*provider.AddGrantResponse, error) { // TODO: update CS3 APIs + // FIXME these should be part of the AddGrantRequest object if req.Opaque != nil { _, spacegrant := req.Opaque.Map["spacegrant"] if spacegrant { ctx = context.WithValue(ctx, utils.SpaceGrant, struct{}{}) } - + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) + } } // check grantee type is valid @@ -1137,39 +926,20 @@ func (s *service) AddGrant(ctx context.Context, req *provider.AddGrantRequest) ( } err := s.storage.AddGrant(ctx, req.Ref, req.Grant) - if err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.NotSupported: - // ignore - setting storage grants is optional - return &provider.AddGrantResponse{ - Status: status.NewOK(ctx), - }, nil - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when setting grants") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error setting grants") - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to add grant") - return &provider.AddGrantResponse{ - Status: st, - }, nil - } - res := &provider.AddGrantResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.AddGrantResponse{ + Status: status.NewStatusFromErrType(ctx, "add grant", err), + }, nil } func (s *service) UpdateGrant(ctx context.Context, req *provider.UpdateGrantRequest) (*provider.UpdateGrantResponse, error) { + // FIXME these should be part of the UpdateGrantRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) + } + } + // check grantee type is valid if req.Grant.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_INVALID { return &provider.UpdateGrantResponse{ @@ -1177,39 +947,21 @@ func (s *service) UpdateGrant(ctx context.Context, req *provider.UpdateGrantRequ }, nil } - if err := s.storage.UpdateGrant(ctx, req.Ref, req.Grant); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.NotSupported: - // ignore - setting storage grants is optional - return &provider.UpdateGrantResponse{ - Status: status.NewOK(ctx), - }, nil - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when updating grant") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error updating grant") - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to update grant") - return &provider.UpdateGrantResponse{ - Status: st, - }, nil - } + err := s.storage.UpdateGrant(ctx, req.Ref, req.Grant) - res := &provider.UpdateGrantResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.UpdateGrantResponse{ + Status: status.NewStatusFromErrType(ctx, "update grant", err), + }, nil } func (s *service) RemoveGrant(ctx context.Context, req *provider.RemoveGrantRequest) (*provider.RemoveGrantResponse, error) { + // FIXME these should be part of the RemoveGrantRequest object + if req.Opaque != nil { + if e, ok := req.Opaque.Map["lockid"]; ok && e.Decoder == "plain" { + ctx = ctxpkg.ContextSetLockID(ctx, string(e.Value)) + } + } + // check targetType is valid if req.Grant.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_INVALID { return &provider.RemoveGrantResponse{ @@ -1217,31 +969,11 @@ func (s *service) RemoveGrant(ctx context.Context, req *provider.RemoveGrantRequ }, nil } - if err := s.storage.RemoveGrant(ctx, req.Ref, req.Grant); err != nil { - var st *rpc.Status - switch err.(type) { - case errtypes.IsNotFound: - st = status.NewNotFound(ctx, "path not found when removing grant") - case errtypes.PermissionDenied: - st = status.NewPermissionDenied(ctx, err, "permission denied") - default: - st = status.NewInternal(ctx, "error removing grant") - } - appctx.GetLogger(ctx). - Error(). - Err(err). - Interface("status", st). - Interface("reference", req.Ref). - Msg("failed to remove grant") - return &provider.RemoveGrantResponse{ - Status: st, - }, nil - } + err := s.storage.RemoveGrant(ctx, req.Ref, req.Grant) - res := &provider.RemoveGrantResponse{ - Status: status.NewOK(ctx), - } - return res, nil + return &provider.RemoveGrantResponse{ + Status: status.NewStatusFromErrType(ctx, "remove grant", err), + }, nil } func (s *service) CreateReference(ctx context.Context, req *provider.CreateReferenceRequest) (*provider.CreateReferenceResponse, error) { diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index 843a467f84f..53889dc8d49 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -128,7 +128,7 @@ func (s *svc) executePathCopy(ctx context.Context, client gateway.GatewayAPIClie 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 := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(log, w, b, err) } return nil @@ -218,7 +218,7 @@ func (s *svc) executePathCopy(ctx context.Context, client gateway.GatewayAPIClie if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(log, w, b, err) return nil } @@ -350,7 +350,7 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, clie w.WriteHeader(http.StatusForbidden) // TODO path could be empty or relative... m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(log, w, b, err) } return nil @@ -426,7 +426,7 @@ func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, clie w.WriteHeader(http.StatusForbidden) // TODO path can be empty or relative m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(log, w, b, err) return nil } @@ -484,7 +484,7 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re if err != nil { w.WriteHeader(http.StatusBadRequest) m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite) - b, err := errors.Marshal(errors.SabredavBadRequest, m, "") + b, err := errors.Marshal(http.StatusBadRequest, m, "") errors.HandleWebdavError(log, w, b, err) return nil } @@ -494,7 +494,7 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re if err != nil { w.WriteHeader(http.StatusBadRequest) m := fmt.Sprintf("Depth header is set to incorrect value %v", dh) - b, err := errors.Marshal(errors.SabredavBadRequest, m, "") + b, err := errors.Marshal(http.StatusBadRequest, m, "") errors.HandleWebdavError(log, w, b, err) return nil } @@ -525,7 +525,7 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re 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 := errors.Marshal(errors.SabredavNotFound, m, "") + b, err := errors.Marshal(http.StatusNotFound, m, "") errors.HandleWebdavError(log, w, b, err) } errors.HandleErrorStatus(log, w, srcStatRes.Status) @@ -552,7 +552,7 @@ func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Re log.Warn().Str("overwrite", overwrite).Msg("dst already exists") w.WriteHeader(http.StatusPreconditionFailed) m := fmt.Sprintf("Could not overwrite Resource %v", dstRef.Path) - b, err := errors.Marshal(errors.SabredavPreconditionFailed, m, "") + b, err := errors.Marshal(http.StatusPreconditionFailed, m, "") errors.HandleWebdavError(log, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 return nil } diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index def7ab68c5f..84f757ddcb4 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -110,9 +110,9 @@ func (h *DavHandler) Handler(s *svc) http.Handler { r.URL.Path = path.Join(r.URL.Path, contextUser.Username) } - if r.Header.Get("Depth") == "" { + if r.Header.Get(net.HeaderDepth) == "" { w.WriteHeader(http.StatusMethodNotAllowed) - b, err := errors.Marshal(errors.SabredavMethodNotAllowed, "Listing members of this collection is disabled", "") + b, err := errors.Marshal(http.StatusMethodNotAllowed, "Listing members of this collection is disabled", "") if err != nil { log.Error().Msgf("error marshaling xml response: %s", b) w.WriteHeader(http.StatusInternalServerError) @@ -208,11 +208,11 @@ func (h *DavHandler) Handler(s *svc) http.Handler { case res.Status.Code == rpcv1beta1.Code_CODE_UNAUTHENTICATED: w.WriteHeader(http.StatusUnauthorized) if hasValidBasicAuthHeader { - b, err := errors.Marshal(errors.SabredavNotAuthenticated, "Username or password was incorrect", "") + b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "") errors.HandleWebdavError(log, w, b, err) return } - b, err := errors.Marshal(errors.SabredavNotAuthenticated, "No 'Authorization: Basic' header found", "") + b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "") errors.HandleWebdavError(log, w, b, err) return case res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: @@ -263,7 +263,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler { default: w.WriteHeader(http.StatusNotFound) - b, err := errors.Marshal(errors.SabredavNotFound, "File not found in root", "") + b, err := errors.Marshal(http.StatusNotFound, "File not found in root", "") errors.HandleWebdavError(log, w, b, err) } }) diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index c8d74248271..4df9b67bb67 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -27,9 +27,12 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/errors" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/net" "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/spacelookup" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rgrpc/status" rtrace "github.com/cs3org/reva/pkg/trace" + "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" ) @@ -58,49 +61,72 @@ func (s *svc) handlePathDelete(w http.ResponseWriter, r *http.Request, ns string } func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { + + ctx, span := rtrace.Provider.Tracer("reva").Start(ctx, "delete") + defer span.End() + + req := &provider.DeleteRequest{Ref: ref} + + // FIXME the lock token is part of the application level protocol, it should be part of the DeleteRequest message not the opaque + ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf)) + if ok { + if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { + req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lockid", ih.lists[0].conditions[0].Token) + } + } else if r.Header.Get(net.HeaderIf) != "" { + w.WriteHeader(http.StatusBadRequest) + b, err := errors.Marshal(http.StatusBadRequest, "invalid if header", "") + errors.HandleWebdavError(&log, w, b, err) + return + } + client, err := s.getClient() if err != nil { log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - - ctx, span := rtrace.Provider.Tracer("reva").Start(ctx, "delete") - defer span.End() - - req := &provider.DeleteRequest{Ref: ref} res, err := client.Delete(ctx, req) if err != nil { span.RecordError(err) log.Error().Err(err).Msg("error performing delete grpc request") w.WriteHeader(http.StatusInternalServerError) return - } else if res.Status.Code != rpc.Code_CODE_OK { - if res.Status.Code == rpc.Code_CODE_NOT_FOUND { - w.WriteHeader(http.StatusNotFound) - // TODO path might be empty or relative... - m := fmt.Sprintf("Resource %v not found", ref.Path) - b, err := errors.Marshal(errors.SabredavNotFound, m, "") - errors.HandleWebdavError(&log, w, b, err) - } - if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { - w.WriteHeader(http.StatusForbidden) - // TODO path might be empty or relative... - m := fmt.Sprintf("Permission denied to delete %v", ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") - errors.HandleWebdavError(&log, w, b, err) + } + switch res.Status.Code { + case rpc.Code_CODE_OK: + w.WriteHeader(http.StatusNoContent) + case rpc.Code_CODE_NOT_FOUND: + w.WriteHeader(http.StatusNotFound) + // TODO path might be empty or relative... + m := fmt.Sprintf("Resource %v not found", ref.Path) + b, err := errors.Marshal(http.StatusNotFound, m, "") + errors.HandleWebdavError(&log, w, b, err) + case rpc.Code_CODE_PERMISSION_DENIED: + status := http.StatusForbidden + if lockID := utils.ReadPlainFromOpaque(res.Opaque, "lockid"); lockID != "" { + // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // Lock-Token value is a Coded-URL. We add angle brackets. + w.Header().Set("Lock-Token", "<"+lockID+">") + status = http.StatusLocked } - if res.Status.Code == rpc.Code_CODE_INTERNAL && res.Status.Message == "can't delete mount path" { + w.WriteHeader(status) + // TODO path might be empty or relative... + m := fmt.Sprintf("Permission denied to delete %v", ref.Path) + b, err := errors.Marshal(status, m, "") + errors.HandleWebdavError(&log, w, b, err) + case rpc.Code_CODE_INTERNAL: + if res.Status.Message == "can't delete mount path" { w.WriteHeader(http.StatusForbidden) - b, err := errors.Marshal(errors.SabredavPermissionDenied, res.Status.Message, "") + b, err := errors.Marshal(http.StatusForbidden, res.Status.Message, "") errors.HandleWebdavError(&log, w, b, err) } - - errors.HandleErrorStatus(&log, w, res.Status) - return + default: + status := status.HTTPStatusFromCode(res.Status.Code) + w.WriteHeader(status) + b, err := errors.Marshal(status, res.Status.Message, "") + errors.HandleWebdavError(&log, w, b, err) } - - w.WriteHeader(http.StatusNoContent) } func (s *svc) handleSpacesDelete(w http.ResponseWriter, r *http.Request, spaceID string) { diff --git a/internal/http/services/owncloud/ocdav/errors/error.go b/internal/http/services/owncloud/ocdav/errors/error.go index b7fde0d3453..b2dd46c0d44 100644 --- a/internal/http/services/owncloud/ocdav/errors/error.go +++ b/internal/http/services/owncloud/ocdav/errors/error.go @@ -24,55 +24,87 @@ import ( "net/http" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/pkg/errors" "github.com/rs/zerolog" ) -type code int +var sabreException = map[int]string{ -const ( + // the commented states have no corresponding exception in sabre/dav, + // see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception - // SabredavBadRequest maps to HTTP 400 - SabredavBadRequest code = iota - // SabredavMethodNotAllowed maps to HTTP 405 - SabredavMethodNotAllowed - // SabredavNotAuthenticated maps to HTTP 401 - SabredavNotAuthenticated - // SabredavPreconditionFailed maps to HTTP 412 - SabredavPreconditionFailed - // SabredavPermissionDenied maps to HTTP 403 - SabredavPermissionDenied - // SabredavNotFound maps to HTTP 404 - SabredavNotFound - // SabredavConflict maps to HTTP 409 - SabredavConflict -) + // http.StatusMultipleChoices: "Multiple Choices", + // http.StatusMovedPermanently: "Moved Permanently", + // http.StatusFound: "Found", + // http.StatusSeeOther: "See Other", + // http.StatusNotModified: "Not Modified", + // http.StatusUseProxy: "Use Proxy", + // http.StatusTemporaryRedirect: "Temporary Redirect", + // http.StatusPermanentRedirect: "Permanent Redirect", -var ( - codesEnum = []string{ - "Sabre\\DAV\\Exception\\BadRequest", - "Sabre\\DAV\\Exception\\MethodNotAllowed", - "Sabre\\DAV\\Exception\\NotAuthenticated", - "Sabre\\DAV\\Exception\\PreconditionFailed", - "Sabre\\DAV\\Exception\\PermissionDenied", - "Sabre\\DAV\\Exception\\NotFound", - "Sabre\\DAV\\Exception\\Conflict", - } -) + http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest", + http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated", + http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired", + http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches + http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound", + http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed", + // http.StatusNotAcceptable: "Not Acceptable", + // http.StatusProxyAuthRequired: "Proxy Authentication Required", + // http.StatusRequestTimeout: "Request Timeout", + http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri + // http.StatusGone: "Gone", + http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired", + http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed", + // http.StatusRequestEntityTooLarge: "Request Entity Too Large", + // http.StatusRequestURITooLong: "Request URI Too Long", + http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported + http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable", + // http.StatusExpectationFailed: "Expectation Failed", + // http.StatusTeapot: "I'm a teapot", + // http.StatusMisdirectedRequest: "Misdirected Request", + // http.StatusUnprocessableEntity: "Unprocessable Entity", + http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock + // http.StatusFailedDependency: "Failed Dependency", + // http.StatusTooEarly: "Too Early", + // http.StatusUpgradeRequired: "Upgrade Required", + // http.StatusPreconditionRequired: "Precondition Required", + // http.StatusTooManyRequests: "Too Many Requests", + // http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", + // http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons", + + // http.StatusInternalServerError: "Internal Server Error", + http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented", + // http.StatusBadGateway: "Bad Gateway", + http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable", + // http.StatusGatewayTimeout: "Gateway Timeout", + // http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported", + // http.StatusVariantAlsoNegotiates: "Variant Also Negotiates", + http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage", + // http.StatusLoopDetected: "Loop Detected", + // http.StatusNotExtended: "Not Extended", + // http.StatusNetworkAuthenticationRequired: "Network Authentication Required", +} + +// SabreException returns a sabre exception text for the HTTP status code. It returns the empty +// string if the code is unknown. +func SabreException(code int) string { + return sabreException[code] +} // Exception represents a ocdav exception type Exception struct { - Code code + Code int Message string Header string } // Marshal just calls the xml marshaller for a given exception. -func Marshal(code code, message string, header string) ([]byte, error) { +func Marshal(code int, message string, header string) ([]byte, error) { xmlstring, err := xml.Marshal(&ErrorXML{ Xmlnsd: "DAV", Xmlnss: "http://sabredav.org/ns", - Exception: codesEnum[code], + Exception: sabreException[code], Message: message, Header: header, }) @@ -98,44 +130,47 @@ type ErrorXML struct { Header string `xml:"s:header,omitempty"` } -// ErrorInvalidPropfind is an invalid propfind error -var ErrorInvalidPropfind = errors.New("webdav: invalid propfind") - -// ErrInvalidProppatch is an invalid proppatch error -var ErrInvalidProppatch = errors.New("webdav: invalid proppatch") +var ( + // ErrInvalidDepth is an invalid depth header error + ErrInvalidDepth = errors.New("webdav: invalid depth") + // ErrInvalidPropfind is an invalid propfind error + ErrInvalidPropfind = errors.New("webdav: invalid propfind") + // ErrInvalidProppatch is an invalid proppatch error + ErrInvalidProppatch = errors.New("webdav: invalid proppatch") + // ErrInvalidLockInfo is an invalid lock error + ErrInvalidLockInfo = errors.New("webdav: invalid lock info") + // ErrUnsupportedLockInfo is an unsupported lock error + ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info") + // ErrInvalidTimeout is an invalid timeout error + ErrInvalidTimeout = errors.New("webdav: invalid timeout") + // ErrInvalidIfHeader is an invalid if header error + ErrInvalidIfHeader = errors.New("webdav: invalid If header") + // ErrUnsupportedMethod is an unsupported method error + ErrUnsupportedMethod = errors.New("webdav: unsupported method") + // ErrInvalidLockToken is an invalid lock token error + ErrInvalidLockToken = errors.New("webdav: invalid lock token") + // ErrConfirmationFailed is returned by a LockSystem's Confirm method. + ErrConfirmationFailed = errors.New("webdav: confirmation failed") + // ErrForbidden is returned by a LockSystem's Unlock method. + ErrForbidden = errors.New("webdav: forbidden") + // ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods. + ErrLocked = errors.New("webdav: locked") + // ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods. + ErrNoSuchLock = errors.New("webdav: no such lock") + // ErrNotImplemented is returned when hitting not implemented code paths + ErrNotImplemented = errors.New("webdav: not implemented") +) // HandleErrorStatus checks the status code, logs a Debug or Error level message // and writes an appropriate http status func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) { - switch s.Code { - case rpc.Code_CODE_OK: - log.Debug().Interface("status", s).Msg("ok") - w.WriteHeader(http.StatusOK) - case rpc.Code_CODE_NOT_FOUND: - log.Debug().Interface("status", s).Msg("resource not found") - w.WriteHeader(http.StatusNotFound) - case rpc.Code_CODE_PERMISSION_DENIED: - log.Debug().Interface("status", s).Msg("permission denied") - w.WriteHeader(http.StatusForbidden) - case rpc.Code_CODE_UNAUTHENTICATED: - log.Debug().Interface("status", s).Msg("unauthenticated") - w.WriteHeader(http.StatusUnauthorized) - case rpc.Code_CODE_INVALID_ARGUMENT: - log.Debug().Interface("status", s).Msg("bad request") - w.WriteHeader(http.StatusBadRequest) - case rpc.Code_CODE_UNIMPLEMENTED: - log.Debug().Interface("status", s).Msg("not implemented") - w.WriteHeader(http.StatusNotImplemented) - case rpc.Code_CODE_INSUFFICIENT_STORAGE: - log.Debug().Interface("status", s).Msg("insufficient storage") - w.WriteHeader(http.StatusInsufficientStorage) - case rpc.Code_CODE_FAILED_PRECONDITION: - log.Debug().Interface("status", s).Msg("destination does not exist") - w.WriteHeader(http.StatusConflict) - default: - log.Error().Interface("status", s).Msg("grpc request failed") - w.WriteHeader(http.StatusInternalServerError) + hsc := status.HTTPStatusFromCode(s.Code) + if hsc == http.StatusInternalServerError { + log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) + } else { + log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc)) } + w.WriteHeader(hsc) } // HandleWebdavError checks the status code, logs an error and creates a webdav response body diff --git a/internal/http/services/owncloud/ocdav/if.go b/internal/http/services/owncloud/ocdav/if.go new file mode 100644 index 00000000000..c331fcbc988 --- /dev/null +++ b/internal/http/services/owncloud/ocdav/if.go @@ -0,0 +1,193 @@ +// 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. + +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ocdav + +// copy of https://github.com/golang/net/blob/master/webdav/if.go + +// The If header is covered by Section 10.4. +// http://www.webdav.org/specs/rfc4918.html#HEADER_If + +import ( + "strings" +) + +// ifHeader is a disjunction (OR) of ifLists. +type ifHeader struct { + lists []ifList +} + +// ifList is a conjunction (AND) of Conditions, and an optional resource tag. +type ifList struct { + resourceTag string + conditions []Condition +} + +// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string +// should omit the "If:" prefix and have any "\r\n"s collapsed to a " ", as is +// returned by req.Header.Get("If") for a http.Request req. +func parseIfHeader(httpHeader string) (h ifHeader, ok bool) { + s := strings.TrimSpace(httpHeader) + switch tokenType, _, _ := lex(s); tokenType { + case '(': + return parseNoTagLists(s) + case angleTokenType: + return parseTaggedLists(s) + default: + return ifHeader{}, false + } +} + +func parseNoTagLists(s string) (h ifHeader, ok bool) { + for { + l, remaining, ok := parseList(s) + if !ok { + return ifHeader{}, false + } + h.lists = append(h.lists, l) + if remaining == "" { + return h, true + } + s = remaining + } +} + +func parseTaggedLists(s string) (h ifHeader, ok bool) { + resourceTag, n := "", 0 + for first := true; ; first = false { + tokenType, tokenStr, remaining := lex(s) + switch tokenType { + case angleTokenType: + if !first && n == 0 { + return ifHeader{}, false + } + resourceTag, n = tokenStr, 0 + s = remaining + case '(': + n++ + l, remaining, ok := parseList(s) + if !ok { + return ifHeader{}, false + } + l.resourceTag = resourceTag + h.lists = append(h.lists, l) + if remaining == "" { + return h, true + } + s = remaining + default: + return ifHeader{}, false + } + } +} + +func parseList(s string) (l ifList, remaining string, ok bool) { + tokenType, _, s := lex(s) + if tokenType != '(' { + return ifList{}, "", false + } + for { + tokenType, _, remaining = lex(s) + if tokenType == ')' { + if len(l.conditions) == 0 { + return ifList{}, "", false + } + return l, remaining, true + } + c, remaining, ok := parseCondition(s) + if !ok { + return ifList{}, "", false + } + l.conditions = append(l.conditions, c) + s = remaining + } +} + +func parseCondition(s string) (c Condition, remaining string, ok bool) { + tokenType, tokenStr, s := lex(s) + if tokenType == notTokenType { + c.Not = true + tokenType, tokenStr, s = lex(s) + } + switch tokenType { + case strTokenType, angleTokenType: + c.Token = tokenStr + case squareTokenType: + c.ETag = tokenStr + default: + return Condition{}, "", false + } + return c, s, true +} + +// Single-rune tokens like '(' or ')' have a token type equal to their rune. +// All other tokens have a negative token type. +const ( + errTokenType = rune(-1) + eofTokenType = rune(-2) + strTokenType = rune(-3) + notTokenType = rune(-4) + angleTokenType = rune(-5) + squareTokenType = rune(-6) +) + +func lex(s string) (tokenType rune, tokenStr string, remaining string) { + // The net/textproto Reader that parses the HTTP header will collapse + // Linear White Space that spans multiple "\r\n" lines to a single " ", + // so we don't need to look for '\r' or '\n'. + for len(s) > 0 && (s[0] == '\t' || s[0] == ' ') { + s = s[1:] + } + if len(s) == 0 { + return eofTokenType, "", "" + } + i := 0 +loop: + for ; i < len(s); i++ { + switch s[i] { + case '\t', ' ', '(', ')', '<', '>', '[', ']': + break loop + } + } + + if i != 0 { + tokenStr, remaining = s[:i], s[i:] + if tokenStr == "Not" { + return notTokenType, "", remaining + } + return strTokenType, tokenStr, remaining + } + + j := 0 + switch s[0] { + case '<': + j, tokenType = strings.IndexByte(s, '>'), angleTokenType + case '[': + j, tokenType = strings.IndexByte(s, ']'), squareTokenType + default: + return rune(s[0]), "", s[1:] + } + if j < 0 { + return errTokenType, "", "" + } + return tokenType, s[1:j], s[j+1:] +} diff --git a/internal/http/services/owncloud/ocdav/if_test.go b/internal/http/services/owncloud/ocdav/if_test.go new file mode 100644 index 00000000000..fcdcb6780e0 --- /dev/null +++ b/internal/http/services/owncloud/ocdav/if_test.go @@ -0,0 +1,338 @@ +// 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 ocdav + +// copy of https://github.com/golang/net/blob/master/webdav/if_test.go + +import ( + "reflect" + "strings" + "testing" +) + +func TestParseIfHeader(t *testing.T) { + // The "section x.y.z" test cases come from section x.y.z of the spec at + // http://www.webdav.org/specs/rfc4918.html + testCases := []struct { + desc string + input string + want ifHeader + }{{ + "bad: empty", + ``, + ifHeader{}, + }, { + "bad: no parens", + `foobar`, + ifHeader{}, + }, { + "bad: empty list #1", + `()`, + ifHeader{}, + }, { + "bad: empty list #2", + `(a) (b c) () (d)`, + ifHeader{}, + }, { + "bad: no list after resource #1", + ``, + ifHeader{}, + }, { + "bad: no list after resource #2", + ` (a)`, + ifHeader{}, + }, { + "bad: no list after resource #3", + ` (a) (b) `, + ifHeader{}, + }, { + "bad: no-tag-list followed by tagged-list", + `(a) (b) (c)`, + ifHeader{}, + }, { + "bad: unfinished list", + `(a`, + ifHeader{}, + }, { + "bad: unfinished ETag", + `([b`, + ifHeader{}, + }, { + "bad: unfinished Notted list", + `(Not a`, + ifHeader{}, + }, { + "bad: double Not", + `(Not Not a)`, + ifHeader{}, + }, { + "good: one list with a Token", + `(a)`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `a`, + }}, + }}, + }, + }, { + "good: one list with an ETag", + `([a])`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + ETag: `a`, + }}, + }}, + }, + }, { + "good: one list with three Nots", + `(Not a Not b Not [d])`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Not: true, + Token: `a`, + }, { + Not: true, + Token: `b`, + }, { + Not: true, + ETag: `d`, + }}, + }}, + }, + }, { + "good: two lists", + `(a) (b)`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `a`, + }}, + }, { + conditions: []Condition{{ + Token: `b`, + }}, + }}, + }, + }, { + "good: two Notted lists", + `(Not a) (Not b)`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Not: true, + Token: `a`, + }}, + }, { + conditions: []Condition{{ + Not: true, + Token: `b`, + }}, + }}, + }, + }, { + "section 7.5.1", + ` + ()`, + ifHeader{ + lists: []ifList{{ + resourceTag: `http://www.example.com/users/f/fielding/index.html`, + conditions: []Condition{{ + Token: `urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6`, + }}, + }}, + }, + }, { + "section 7.5.2 #1", + `()`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + }}, + }}, + }, + }, { + "section 7.5.2 #2", + ` + ()`, + ifHeader{ + lists: []ifList{{ + resourceTag: `http://example.com/locked/`, + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + }}, + }}, + }, + }, { + "section 7.5.2 #3", + ` + ()`, + ifHeader{ + lists: []ifList{{ + resourceTag: `http://example.com/locked/member`, + conditions: []Condition{{ + Token: `urn:uuid:150852e2-3847-42d5-8cbe-0f4f296f26cf`, + }}, + }}, + }, + }, { + "section 9.9.6", + `() + ()`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4`, + }}, + }, { + conditions: []Condition{{ + Token: `urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77`, + }}, + }}, + }, + }, { + "section 9.10.8", + `()`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `urn:uuid:e71d4fae-5dec-22d6-fea5-00a0c91e6be4`, + }}, + }}, + }, + }, { + "section 10.4.6", + `( + ["I am an ETag"]) + (["I am another ETag"])`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + }, { + ETag: `"I am an ETag"`, + }}, + }, { + conditions: []Condition{{ + ETag: `"I am another ETag"`, + }}, + }}, + }, + }, { + "section 10.4.7", + `(Not + )`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Not: true, + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + }, { + Token: `urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092`, + }}, + }}, + }, + }, { + "section 10.4.8", + `() + (Not )`, + ifHeader{ + lists: []ifList{{ + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + }}, + }, { + conditions: []Condition{{ + Not: true, + Token: `DAV:no-lock`, + }}, + }}, + }, + }, { + "section 10.4.9", + ` + ( + [W/"A weak ETag"]) (["strong ETag"])`, + ifHeader{ + lists: []ifList{{ + resourceTag: `/resource1`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + }, { + ETag: `W/"A weak ETag"`, + }}, + }, { + resourceTag: `/resource1`, + conditions: []Condition{{ + ETag: `"strong ETag"`, + }}, + }}, + }, + }, { + "section 10.4.10", + ` + ()`, + ifHeader{ + lists: []ifList{{ + resourceTag: `http://www.example.com/specs/`, + conditions: []Condition{{ + Token: `urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2`, + }}, + }}, + }, + }, { + "section 10.4.11 #1", + ` (["4217"])`, + ifHeader{ + lists: []ifList{{ + resourceTag: `/specs/rfc2518.doc`, + conditions: []Condition{{ + ETag: `"4217"`, + }}, + }}, + }, + }, { + "section 10.4.11 #2", + ` (Not ["4217"])`, + ifHeader{ + lists: []ifList{{ + resourceTag: `/specs/rfc2518.doc`, + conditions: []Condition{{ + Not: true, + ETag: `"4217"`, + }}, + }}, + }, + }} + + for _, tc := range testCases { + got, ok := parseIfHeader(strings.ReplaceAll(tc.input, "\n", "")) + if gotEmpty := reflect.DeepEqual(got, ifHeader{}); gotEmpty == ok { + t.Errorf("%s: should be different: empty header == %t, ok == %t", tc.desc, gotEmpty, ok) + continue + } + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("%s:\ngot %v\nwant %v", tc.desc, got, tc.want) + continue + } + } +} diff --git a/internal/http/services/owncloud/ocdav/lock.go b/internal/http/services/owncloud/ocdav/lock.go deleted file mode 100644 index 65f43cf64c2..00000000000 --- a/internal/http/services/owncloud/ocdav/lock.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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 ocdav - -import ( - "net/http" - - "github.com/cs3org/reva/pkg/appctx" -) - -// TODO(jfd) implement lock -func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) { - log := appctx.GetLogger(r.Context()) - xml := ` - - - - - Second-604800 - Infinity - - opaquelocktoken:00000000-0000-0000-0000-000000000000 - - - - ` - - w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") - w.Header().Set("Lock-Token", - "opaquelocktoken:00000000-0000-0000-0000-000000000000") - _, err := w.Write([]byte(xml)) - if err != nil { - log.Err(err).Msg("error writing response") - } -} diff --git a/internal/http/services/owncloud/ocdav/locks.go b/internal/http/services/owncloud/ocdav/locks.go new file mode 100644 index 00000000000..daee6d8dba1 --- /dev/null +++ b/internal/http/services/owncloud/ocdav/locks.go @@ -0,0 +1,608 @@ +// 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 ocdav + +import ( + "context" + "encoding/xml" + "fmt" + "io" + "net/http" + "path" + "strconv" + "strings" + "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/errors" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/net" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/props" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/spacelookup" + "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/errtypes" + rtrace "github.com/cs3org/reva/pkg/trace" + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" +) + +// Most of this is taken from https://github.com/golang/net/blob/master/webdav/lock.go + +// From RFC4918 http://www.webdav.org/specs/rfc4918.html#lock-tokens +// This specification encourages servers to create Universally Unique Identifiers (UUIDs) for lock tokens, +// and to use the URI form defined by "A Universally Unique Identifier (UUID) URN Namespace" ([RFC4122]). +// However, servers are free to use any URI (e.g., from another scheme) so long as it meets the uniqueness +// requirements. For example, a valid lock token might be constructed using the "opaquelocktoken" scheme +// defined in Appendix C. +// +// Example: "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6" +// +// we stick to the recommendation and use the URN Namespace +const lockTokenPrefix = "urn:uuid:" + +// TODO(jfd) implement lock +// see Web Distributed Authoring and Versioning (WebDAV) Locking Protocol: +// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html +// Webdav supports a Depth: infinity lock, wopi only needs locks on files + +// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html#write.locks.and.the.if.request.header +// [...] a lock token MUST be submitted in the If header for all locked resources +// that a method may interact with or the method MUST fail. [...] +/* + COPY /~fielding/index.html HTTP/1.1 + Host: example.com + Destination: http://example.com/users/f/fielding/index.html + If: + () +*/ + +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo +type lockInfo struct { + XMLName xml.Name `xml:"lockinfo"` + Exclusive *struct{} `xml:"lockscope>exclusive"` + Shared *struct{} `xml:"lockscope>shared"` + Write *struct{} `xml:"locktype>write"` + Owner owner `xml:"owner"` +} + +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner +type owner struct { + InnerXML string `xml:",innerxml"` +} + +// Condition can match a WebDAV resource, based on a token or ETag. +// Exactly one of Token and ETag should be non-empty. +type Condition struct { + Not bool + Token string + ETag string +} + +// LockSystem manages access to a collection of named resources. The elements +// in a lock name are separated by slash ('/', U+002F) characters, regardless +// of host operating system convention. +type LockSystem interface { + // Confirm confirms that the caller can claim all of the locks specified by + // the given conditions, and that holding the union of all of those locks + // gives exclusive access to all of the named resources. Up to two resources + // can be named. Empty names are ignored. + // + // Exactly one of release and err will be non-nil. If release is non-nil, + // all of the requested locks are held until release is called. Calling + // release does not unlock the lock, in the WebDAV UNLOCK sense, but once + // Confirm has confirmed that a lock claim is valid, that lock cannot be + // Confirmed again until it has been released. + // + // If Confirm returns ErrConfirmationFailed then the Handler will continue + // to try any other set of locks presented (a WebDAV HTTP request can + // present more than one set of locks). If it returns any other non-nil + // error, the Handler will write a "500 Internal Server Error" HTTP status. + Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error) + + // Create creates a lock with the given depth, duration, owner and root + // (name). The depth will either be negative (meaning infinite) or zero. + // + // If Create returns ErrLocked then the Handler will write a "423 Locked" + // HTTP status. If it returns any other non-nil error, the Handler will + // write a "500 Internal Server Error" HTTP status. + // + // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for + // when to use each error. + // + // The token returned identifies the created lock. It should be an absolute + // URI as defined by RFC 3986, Section 4.3. In particular, it should not + // contain whitespace. + Create(ctx context.Context, now time.Time, details LockDetails) (token string, err error) + + // Refresh refreshes the lock with the given token. + // + // If Refresh returns ErrLocked then the Handler will write a "423 Locked" + // HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write + // a "412 Precondition Failed" HTTP Status. If it returns any other non-nil + // error, the Handler will write a "500 Internal Server Error" HTTP status. + // + // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for + // when to use each error. + Refresh(ctx context.Context, now time.Time, token string, duration time.Duration) (LockDetails, error) + + // Unlock unlocks the lock with the given token. + // + // If Unlock returns ErrForbidden then the Handler will write a "403 + // Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler + // will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock + // then the Handler will write a "409 Conflict" HTTP Status. If it returns + // any other non-nil error, the Handler will write a "500 Internal Server + // Error" HTTP status. + // + // See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for + // when to use each error. + Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error +} + +// NewCS3LS returns a new CS3 based LockSystem. +func NewCS3LS(c gateway.GatewayAPIClient) LockSystem { + return &cs3LS{ + client: c, + } +} + +type cs3LS struct { + client gateway.GatewayAPIClient +} + +func (cls *cs3LS) Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (func(), error) { + return nil, errors.ErrNotImplemented +} + +func (cls *cs3LS) Create(ctx context.Context, now time.Time, details LockDetails) (string, error) { + // always assume depth infinity? + /* + if !details.ZeroDepth { + The CS3 Lock api currently has no depth property, it only locks single resources + return "", errors.ErrUnsupportedLockInfo + } + */ + + // Having a lock token provides no special access rights. Anyone can find out anyone + // else's lock token by performing lock discovery. Locks must be enforced based upon + // whatever authentication mechanism is used by the server, not based on the secrecy + // of the token values. + // see: http://www.webdav.org/specs/rfc2518.html#n-lock-tokens + token := uuid.New() + + r := &provider.SetLockRequest{ + Ref: details.Root, + Lock: &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: details.UserID, // no way to set an app lock? TODO maybe via the ownerxml + //AppName: , // TODO use a urn scheme? + LockId: lockTokenPrefix + token.String(), // can be a token or a Coded-URL + }, + } + if details.Duration > 0 { + expiration := time.Now().UTC().Add(details.Duration) + r.Lock.Expiration = &types.Timestamp{ + Seconds: uint64(expiration.Unix()), + Nanos: uint32(expiration.Nanosecond()), + } + } + res, err := cls.client.SetLock(ctx, r) + if err != nil { + return "", err + } + if res.Status.Code != rpc.Code_CODE_OK { + return "", errtypes.NewErrtypeFromStatus(res.Status) + } + return lockTokenPrefix + token.String(), nil +} + +func (cls *cs3LS) Refresh(ctx context.Context, now time.Time, token string, duration time.Duration) (LockDetails, error) { + return LockDetails{}, errors.ErrNotImplemented +} +func (cls *cs3LS) Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error { + r := &provider.UnlockRequest{ + Ref: ref, + Lock: &provider.Lock{ + LockId: token, // can be a token or a Coded-URL + }, + } + res, err := cls.client.Unlock(ctx, r) + if err != nil { + return err + } + if res.Status.Code != rpc.Code_CODE_OK { + return errtypes.NewErrtypeFromStatus(res.Status) + } + return nil +} + +// LockDetails are a lock's metadata. +type LockDetails struct { + // Root is the root resource name being locked. For a zero-depth lock, the + // root is the only resource being locked. + Root *provider.Reference + // Duration is the lock timeout. A negative duration means infinite. + Duration time.Duration + // OwnerXML is the verbatim XML given in a LOCK HTTP request. + // + // TODO: does the "verbatim" nature play well with XML namespaces? + // Does the OwnerXML field need to have more structure? See + // https://codereview.appspot.com/175140043/#msg2 + OwnerXML string + UserID *userpb.UserId + // ZeroDepth is whether the lock has zero depth. If it does not have zero + // depth, it has infinite depth. + ZeroDepth bool +} + +func readLockInfo(r io.Reader) (li lockInfo, status int, err error) { + c := &countingReader{r: r} + if err = xml.NewDecoder(c).Decode(&li); err != nil { + if err == io.EOF { + if c.n == 0 { + // An empty body means to refresh the lock. + // http://www.webdav.org/specs/rfc4918.html#refreshing-locks + return lockInfo{}, 0, nil + } + err = errors.ErrInvalidLockInfo + } + return lockInfo{}, http.StatusBadRequest, err + } + // We only support exclusive (non-shared) write locks. In practice, these are + // the only types of locks that seem to matter. + // We are ignoring the any properties in the lock details, and assume an exclusive write lock is requested. + // https://datatracker.ietf.org/doc/html/rfc4918#section-7 only describes write locks + // + // if li.Exclusive == nil || li.Shared != nil { + // return lockInfo{}, http.StatusNotImplemented, errors.ErrUnsupportedLockInfo + // } + // what should we return if the user requests a shared lock? or leaves out the locktype? the testsuite will only send the property lockscope, not locktype + // the oc tests cover both shared and exclusive locks. What is the WOPI lock? a shared or an exclusive lock? + // since it is issued by a service it seems to be an exclusive lock. + // the owner could be a link to the collaborative app ... to join the session + return li, 0, nil +} + +type countingReader struct { + n int + r io.Reader +} + +func (c *countingReader) Read(p []byte) (int, error) { + n, err := c.r.Read(p) + c.n += n + return n, err +} + +const infiniteTimeout = -1 + +// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is +// empty, an infiniteTimeout is returned. +func parseTimeout(s string) (time.Duration, error) { + if s == "" { + return infiniteTimeout, nil + } + if i := strings.IndexByte(s, ','); i >= 0 { + s = s[:i] + } + s = strings.TrimSpace(s) + if s == "Infinite" { + return infiniteTimeout, nil + } + const pre = "Second-" + if !strings.HasPrefix(s, pre) { + return 0, errors.ErrInvalidTimeout + } + s = s[len(pre):] + if s == "" || s[0] < '0' || '9' < s[0] { + return 0, errors.ErrInvalidTimeout + } + n, err := strconv.ParseInt(s, 10, 64) + if err != nil || 1<<32-1 < n { + return 0, errors.ErrInvalidTimeout + } + return time.Duration(n) * time.Second, nil +} + +const ( + infiniteDepth = -1 + invalidDepth = -2 +) + +// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and +// infiniteDepth. Parsing any other string returns invalidDepth. +// +// Different WebDAV methods have further constraints on valid depths: +// - PROPFIND has no further restrictions, as per section 9.1. +// - COPY accepts only "0" or "infinity", as per section 9.8.3. +// - MOVE accepts only "infinity", as per section 9.9.2. +// - LOCK accepts only "0" or "infinity", as per section 9.10.3. +// These constraints are enforced by the handleXxx methods. +func parseDepth(s string) int { + switch s { + case "0": + return 0 + case "1": + return 1 + case "infinity": + return infiniteDepth + } + return invalidDepth +} + +/* + the oc 10 wopi app code locks like this: + + $storage->lockNodePersistent($file->getInternalPath(), [ + 'token' => $wopiLock, + 'owner' => "{$user->getDisplayName()} via Office Online" + ]); + + if owner is empty it defaults to '{displayname} ({email})', which is not a url ... but ... shrug + + The LockManager also defaults to exclusive locks: + + $scope = ILock::LOCK_SCOPE_EXCLUSIVE; + if (isset($lockInfo['scope'])) { + $scope = $lockInfo['scope']; + } +*/ +func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) (retStatus int, retErr error) { + ctx, span := rtrace.Provider.Tracer("reva").Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) + defer span.End() + + span.SetAttributes(attribute.String("component", "ocdav")) + + fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces? + + client, err := s.getClient() + if err != nil { + return http.StatusInternalServerError, err + } + + // TODO instead of using a string namespace ns pass in the space with the request? + ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, client, fn) + if err != nil { + return http.StatusInternalServerError, err + } + if cs3Status.Code != rpc.Code_CODE_OK { + return http.StatusInternalServerError, errtypes.NewErrtypeFromStatus(cs3Status) + } + + return s.lockReference(ctx, w, r, ref) +} + +func (s *svc) handleSpacesLock(w http.ResponseWriter, r *http.Request, spaceID string) (retStatus int, retErr error) { + ctx, span := rtrace.Provider.Tracer("reva").Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) + defer span.End() + + span.SetAttributes(attribute.String("component", "ocdav")) + + client, err := s.getClient() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // retrieve a specific storage space + space, cs3Status, err := spacelookup.LookUpStorageSpaceByID(ctx, client, spaceID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if cs3Status.Code != rpc.Code_CODE_OK { + return http.StatusInternalServerError, errtypes.NewErrtypeFromStatus(cs3Status) + } + + ref := spacelookup.MakeRelativeReference(space, r.URL.Path, true) + + return s.lockReference(ctx, w, r, ref) +} + +func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) { + sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger() + duration, err := parseTimeout(r.Header.Get(net.HeaderTimeout)) + if err != nil { + return http.StatusBadRequest, errors.ErrInvalidTimeout + } + + li, status, err := readLockInfo(r.Body) + if err != nil { + return status, errors.ErrInvalidLockInfo + } + + u := ctxpkg.ContextMustGetUser(ctx) + token, ld, now, created := "", LockDetails{UserID: u.Id, Root: ref, Duration: duration}, time.Now(), false + if li == (lockInfo{}) { + // An empty lockInfo means to refresh the lock. + ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf)) + if !ok { + return http.StatusBadRequest, errors.ErrInvalidIfHeader + } + if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { + token = ih.lists[0].conditions[0].Token + } + if token == "" { + return http.StatusBadRequest, errors.ErrInvalidLockToken + } + ld, err = s.LockSystem.Refresh(ctx, now, token, duration) + if err != nil { + if err == errors.ErrNoSuchLock { + return http.StatusPreconditionFailed, err + } + return http.StatusInternalServerError, err + } + + } else { + // Section 9.10.3 says that "If no Depth header is submitted on a LOCK request, + // then the request MUST act as if a "Depth:infinity" had been submitted." + depth := infiniteDepth + if hdr := r.Header.Get(net.HeaderDepth); hdr != "" { + depth = parseDepth(hdr) + if depth != 0 && depth != infiniteDepth { + // Section 9.10.3 says that "Values other than 0 or infinity must not be + // used with the Depth header on a LOCK method". + return http.StatusBadRequest, errors.ErrInvalidDepth + } + } + /* our url path has been shifted, so we don't need to do this? + reqPath, status, err := h.stripPrefix(r.URL.Path) + if err != nil { + return status, err + } + */ + // TODO look up username and email + // if li.Owner.InnerXML == "" { + // // PHP version: 'owner' => "{$user->getDisplayName()} via Office Online" + // ld.OwnerXML = ld.UserID.OpaqueId + // } + ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL + ld.ZeroDepth = depth == 0 + + //TODO: @jfd the code tries to create a lock for a file that may not even exist, + // should we do that in the decomposedfs as well? the node does not exist + // this actually is a name based lock ... ugh + token, err = s.LockSystem.Create(ctx, now, ld) + if err != nil { + if _, ok := err.(errtypes.Locked); ok { + return http.StatusLocked, err + } + return http.StatusInternalServerError, err + } + + defer func() { + if retErr != nil { + if err := s.LockSystem.Unlock(ctx, now, ref, token); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("lock", ld).Msg("could not unlock after failed lock") + } + } + }() + + // Create the resource if it didn't previously exist. + // TODO use sdk to stat? + /* + if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil { + f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + // TODO: detect missing intermediate dirs and return http.StatusConflict? + return http.StatusInternalServerError, err + } + f.Close() + created = true + } + */ + // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // Lock-Token value is a Coded-URL. We add angle brackets. + w.Header().Set("Lock-Token", "<"+token+">") + } + + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + if created { + // This is "w.WriteHeader(http.StatusCreated)" and not "return + // http.StatusCreated, nil" because we write our own (XML) response to w + // and Handler.ServeHTTP would otherwise write "Created". + w.WriteHeader(http.StatusCreated) + } + n, err := writeLockInfo(w, token, ld) + if err != nil { + sublog.Err(err).Int("bytes_written", n).Msg("error writing response") + } + return 0, nil +} + +func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) { + depth := "infinity" + if ld.ZeroDepth { + depth = "0" + } + href := ld.Root.Path // FIXME add base url and space? + + lockdiscovery := strings.Builder{} + lockdiscovery.WriteString(xml.Header) + lockdiscovery.WriteString("\n") + lockdiscovery.WriteString(" \n") + lockdiscovery.WriteString(" \n") + lockdiscovery.WriteString(fmt.Sprintf(" %s\n", depth)) + if ld.OwnerXML != "" { + lockdiscovery.WriteString(fmt.Sprintf(" %s\n", ld.OwnerXML)) + } + if ld.Duration > 0 { + timeout := ld.Duration / time.Second + lockdiscovery.WriteString(fmt.Sprintf(" Second-%d\n", timeout)) + } else { + lockdiscovery.WriteString(" Infinite\n") + } + if token != "" { + lockdiscovery.WriteString(fmt.Sprintf(" %s\n", props.Escape(token))) + } + if href != "" { + lockdiscovery.WriteString(fmt.Sprintf(" %s\n", props.Escape(href))) + } + lockdiscovery.WriteString("") + + return fmt.Fprint(w, lockdiscovery.String()) +} + +func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) { + ctx, span := rtrace.Provider.Tracer("reva").Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path)) + defer span.End() + + span.SetAttributes(attribute.String("component", "ocdav")) + + fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces? + + client, err := s.getClient() + if err != nil { + return http.StatusInternalServerError, err + } + + // TODO instead of using a string namespace ns pass in the space with the request? + ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, client, fn) + if err != nil { + return http.StatusInternalServerError, err + } + if cs3Status.Code != rpc.Code_CODE_OK { + return http.StatusInternalServerError, errtypes.NewErrtypeFromStatus(cs3Status) + } + + // http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the + // Lock-Token value should be a Coded-URL OR a token. We strip its angle brackets. + t := r.Header.Get(net.HeaderLockToken) + if len(t) > 2 && t[0] == '<' && t[len(t)-1] == '>' { + t = t[1 : len(t)-1] + } + + switch err = s.LockSystem.Unlock(r.Context(), time.Now(), ref, t); err { + case nil: + return http.StatusNoContent, err + case errors.ErrForbidden: + return http.StatusForbidden, err + case errors.ErrLocked: + return http.StatusLocked, err + case errors.ErrNoSuchLock: + return http.StatusConflict, err + default: + return http.StatusInternalServerError, err + } +} diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go index 23d1b3fd59b..2d55f1b3e2d 100644 --- a/internal/http/services/owncloud/ocdav/mkcol.go +++ b/internal/http/services/owncloud/ocdav/mkcol.go @@ -69,7 +69,7 @@ func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) sublog.Info().Err(err).Str("path", fn).Interface("code", sr.Status.Code).Msg("response code for stat was unexpected") // tests want this errorcode. StatusConflict would be more logical w.WriteHeader(http.StatusMethodNotAllowed) - b, err := errors.Marshal(errors.SabredavMethodNotAllowed, "The resource you tried to create already exists", "") + b, err := errors.Marshal(http.StatusMethodNotAllowed, "The resource you tried to create already exists", "") errors.HandleWebdavError(&sublog, w, b, err) return } @@ -159,7 +159,7 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re // all ancestors must already exist, or the method must fail // with a 409 (Conflict) status code. w.WriteHeader(http.StatusConflict) - b, err := errors.Marshal(errors.SabredavNotFound, "Parent node does not exist", "") + b, err := errors.Marshal(http.StatusConflict, "Parent node does not exist", "") errors.HandleWebdavError(&log, w, b, err) } else { errors.HandleErrorStatus(&log, w, parentStatRes.Status) @@ -179,7 +179,7 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re if statRes.Status.Code != rpc.Code_CODE_NOT_FOUND { if statRes.Status.Code == rpc.Code_CODE_OK { w.WriteHeader(http.StatusMethodNotAllowed) // 405 if it already exists - b, err := errors.Marshal(errors.SabredavMethodNotAllowed, "The resource you tried to create already exists", "") + b, err := errors.Marshal(http.StatusMethodNotAllowed, "The resource you tried to create already exists", "") errors.HandleWebdavError(&log, w, b, err) } else { errors.HandleErrorStatus(&log, w, statRes.Status) @@ -204,7 +204,7 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re w.WriteHeader(http.StatusForbidden) // TODO path could be empty or relative... m := fmt.Sprintf("Permission denied to create %v", childRef.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(&log, w, b, err) default: errors.HandleErrorStatus(&log, w, res.Status) diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index d68ffff23e8..8947ff8bf9a 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -177,7 +177,7 @@ func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Req 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 := errors.Marshal(errors.SabredavNotFound, m, "") + b, err := errors.Marshal(http.StatusNotFound, m, "") errors.HandleWebdavError(&log, w, b, err) } errors.HandleErrorStatus(&log, w, srcStatRes.Status) @@ -257,7 +257,7 @@ func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Req if mRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) m := fmt.Sprintf("Permission denied to move %v", src.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(&log, w, b, err) } errors.HandleErrorStatus(&log, w, mRes.Status) diff --git a/internal/http/services/owncloud/ocdav/net/headers.go b/internal/http/services/owncloud/ocdav/net/headers.go index 657ad770202..8e5798ff92c 100644 --- a/internal/http/services/owncloud/ocdav/net/headers.go +++ b/internal/http/services/owncloud/ocdav/net/headers.go @@ -34,25 +34,33 @@ const ( HeaderIfMatch = "If-Match" ) +// webdav headers +const ( + HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1 + HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2 + HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3 + HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4 + HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5 + HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6 + HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7 +) + // Non standard HTTP headers. const ( HeaderOCFileID = "OC-FileId" HeaderOCETag = "OC-ETag" HeaderOCChecksum = "OC-Checksum" HeaderOCPermissions = "OC-Perm" - HeaderDepth = "Depth" - HeaderDav = "DAV" HeaderTusResumable = "Tus-Resumable" HeaderTusVersion = "Tus-Version" HeaderTusExtension = "Tus-Extension" HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm" HeaderTusUploadExpires = "Upload-Expires" - HeaderDestination = "Destination" - HeaderOverwrite = "Overwrite" HeaderUploadChecksum = "Upload-Checksum" HeaderUploadLength = "Upload-Length" HeaderUploadMetadata = "Upload-Metadata" HeaderUploadOffset = "Upload-Offset" HeaderOCMtime = "X-OC-Mtime" HeaderExpectedEntityLength = "X-Expected-Entity-Length" + HeaderLitmus = "X-Litmus" ) diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index ae2e5ac570c..fd1303e1a11 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -114,6 +114,8 @@ type svc struct { davHandler *DavHandler favoritesManager favorite.Manager client *http.Client + // LockSystem is the lock management system. + LockSystem LockSystem } func (s *svc) Config() *Config { @@ -126,6 +128,14 @@ func getFavoritesManager(c *Config) (favorite.Manager, error) { } return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver) } +func getLockSystem(c *Config) (LockSystem, error) { + // TODO in memory implementation + client, err := pool.GetGatewayServiceClient(c.GatewaySvc) + if err != nil { + return nil, err + } + return NewCS3LS(client), nil +} // New returns a new ocdav func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { @@ -140,6 +150,10 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) if err != nil { return nil, err } + ls, err := getLockSystem(conf) + if err != nil { + return nil, err + } s := &svc{ c: conf, @@ -150,6 +164,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) rhttp.Insecure(conf.Insecure), ), favoritesManager: fm, + LockSystem: ls, } // initialize handlers and set default configs if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil { @@ -182,7 +197,7 @@ func (s *svc) Handler() http.Handler { // TODO(jfd): do we need this? // fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89 - if r.Header.Get("X-Litmus") == "props: 3 (propfind_invalid2)" { + if r.Header.Get(net.HeaderLitmus) == "props: 3 (propfind_invalid2)" { http.Error(w, "400 Bad Request", http.StatusBadRequest) return } diff --git a/internal/http/services/owncloud/ocdav/propfind/propfind.go b/internal/http/services/owncloud/ocdav/propfind/propfind.go index 874ba81e80d..b1aadcf8ee8 100644 --- a/internal/http/services/owncloud/ocdav/propfind/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind/propfind.go @@ -244,7 +244,7 @@ func (p *Handler) getResourceInfos(ctx context.Context, w http.ResponseWriter, r log.Debug().Str("depth", dh).Msg(err.Error()) w.WriteHeader(http.StatusBadRequest) m := fmt.Sprintf("Invalid Depth header value: %v", dh) - b, err := errors.Marshal(errors.SabredavBadRequest, m, "") + b, err := errors.Marshal(http.StatusBadRequest, m, "") errors.HandleWebdavError(&log, w, b, err) return nil, false, false } @@ -326,7 +326,7 @@ func (p *Handler) getResourceInfos(ctx context.Context, w http.ResponseWriter, r // TODO if we have children invent node on the fly w.WriteHeader(http.StatusNotFound) m := fmt.Sprintf("Resource %v not found", requestPath) - b, err := errors.Marshal(errors.SabredavNotFound, m, "") + b, err := errors.Marshal(http.StatusNotFound, m, "") errors.HandleWebdavError(&log, w, b, err) return nil, false, false } @@ -348,6 +348,12 @@ func (p *Handler) getResourceInfos(ctx context.Context, w http.ResponseWriter, r resourceInfos := []*provider.ResourceInfo{ rootInfo, // PROPFIND always includes the root resource } + + if rootInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + // no need to stat any other spaces, we got our file stat already + return resourceInfos, true, true + } + childInfos := map[string]*provider.ResourceInfo{} // then add children @@ -524,19 +530,19 @@ func ReadPropfind(r io.Reader) (pf XML, status int, err error) { // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND return XML{Allprop: new(struct{})}, 0, nil } - err = errors.ErrorInvalidPropfind + err = errors.ErrInvalidPropfind } return XML{}, http.StatusBadRequest, err } if pf.Allprop == nil && pf.Include != nil { - return XML{}, http.StatusBadRequest, errors.ErrorInvalidPropfind + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind } if pf.Allprop != nil && (pf.Prop != nil || pf.Propname != nil) { - return XML{}, http.StatusBadRequest, errors.ErrorInvalidPropfind + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind } if pf.Prop != nil && pf.Propname != nil { - return XML{}, http.StatusBadRequest, errors.ErrorInvalidPropfind + return XML{}, http.StatusBadRequest, errors.ErrInvalidPropfind } if pf.Propname == nil && pf.Allprop == nil && pf.Prop == nil { // jfd: I think is perfectly valid ... treat it as allprop @@ -594,6 +600,7 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p // -3 indicates unlimited quota := net.PropQuotaUnknown size := strconv.FormatUint(md.Size, 10) + var lock *provider.Lock // TODO refactor helper functions: GetOpaqueJSONEncoded(opaque, key string, *struct) err, GetOpaquePlainEncoded(opaque, key) value, err // or use ok like pattern and return bool? if md.Opaque != nil && md.Opaque.Map != nil { @@ -607,6 +614,13 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p if md.Opaque.Map["quota"] != nil && md.Opaque.Map["quota"].Decoder == "plain" { quota = string(md.Opaque.Map["quota"].Value) } + if md.Opaque.Map["lock"] != nil && md.Opaque.Map["lock"].Decoder == "json" { + lock = &provider.Lock{} + err := json.Unmarshal(md.Opaque.Map["lock"].Value, lock) + if err != nil { + sublog.Error().Err(err).Msg("could not unmarshal locks json") + } + } } role := conversions.RoleFromResourcePermissions(md.PermissionSet) @@ -725,6 +739,10 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p propstatOK.Prop = append(propstatOK.Prop, props.NewProp("oc:favorite", "0")) } } + + if lock != nil { + propstatOK.Prop = append(propstatOK.Prop, props.NewPropRaw("d:lockdiscovery", activeLocks(&sublog, lock))) + } // TODO return other properties ... but how do we put them in a namespace? } else { // otherwise return only the requested properties @@ -1026,6 +1044,12 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p } else { propstatNotFound.Prop = append(propstatNotFound.Prop, props.NewProp("d:quota-available-bytes", "")) } + case "lockdiscovery": // http://www.webdav.org/specs/rfc2518.html#PROPERTY_lockdiscovery + if lock == nil { + propstatNotFound.Prop = append(propstatNotFound.Prop, props.NewProp("d:lockdiscovery", "")) + } else { + propstatOK.Prop = append(propstatOK.Prop, props.NewPropRaw("d:lockdiscovery", activeLocks(&sublog, lock))) + } default: propstatNotFound.Prop = append(propstatNotFound.Prop, props.NewProp("d:"+pf.Prop[i].Local, "")) } @@ -1076,6 +1100,78 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p return &response, nil } +func activeLocks(log *zerolog.Logger, lock *provider.Lock) string { + if lock == nil || lock.Type == provider.LockType_LOCK_TYPE_INVALID { + return "" + } + expiration := "Infinity" + if lock.Expiration != nil { + now := uint64(time.Now().Unix()) + // Should we hide expired locks here? No. + // + // If the timeout expires, then the lock SHOULD be removed. In this + // case the server SHOULD act as if an UNLOCK method was executed by the + // server on the resource using the lock token of the timed-out lock, + // performed with its override authority. + // + // see https://datatracker.ietf.org/doc/html/rfc4918#section-6.6 + if lock.Expiration.Seconds >= now { + expiration = "Second-" + strconv.FormatUint(lock.Expiration.Seconds-now, 10) + } else { + expiration = "Second-0" + } + } + + // xml.Encode cannot render emptytags like , see https://github.com/golang/go/issues/21399 + var activelocks strings.Builder + activelocks.WriteString("") + // webdav locktype write | transaction + switch lock.Type { + case provider.LockType_LOCK_TYPE_EXCL: + fallthrough + case provider.LockType_LOCK_TYPE_WRITE: + activelocks.WriteString("") + } + // webdav lockscope exclusive, shared, or local + switch lock.Type { + case provider.LockType_LOCK_TYPE_EXCL: + fallthrough + case provider.LockType_LOCK_TYPE_WRITE: + activelocks.WriteString("") + case provider.LockType_LOCK_TYPE_SHARED: + activelocks.WriteString("") + } + // we currently only support depth infinity + activelocks.WriteString("Infinity") + + if lock.User != nil || lock.AppName != "" { + activelocks.WriteString("") + + if lock.User != nil { + // TODO oc10 uses displayname and email, needs a user lookup + activelocks.WriteString(props.Escape(lock.User.OpaqueId + "@" + lock.User.Idp)) + } + if lock.AppName != "" { + if lock.User != nil { + activelocks.WriteString(" via ") + } + activelocks.WriteString(props.Escape(lock.AppName)) + } + activelocks.WriteString("") + } + activelocks.WriteString("") + activelocks.WriteString(expiration) + activelocks.WriteString("") + if lock.LockId != "" { + activelocks.WriteString("") + activelocks.WriteString(props.Escape(lock.LockId)) + activelocks.WriteString("") + } + // lockroot is only used when setting the lock + activelocks.WriteString("") + return activelocks.String() +} + // be defensive about wrong encoded etags func quoteEtag(etag string) string { if strings.HasPrefix(etag, "W/") { diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index 9724a26a47b..696331cfc16 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -54,7 +54,7 @@ func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns str sublog.Debug().Err(err).Msg("error reading proppatch") w.WriteHeader(status) m := fmt.Sprintf("Error reading proppatch: %v", err) - b, err := errors.Marshal(errors.SabredavBadRequest, m, "") + b, err := errors.Marshal(status, m, "") errors.HandleWebdavError(&sublog, w, b, err) return } @@ -89,7 +89,7 @@ func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns str if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { w.WriteHeader(http.StatusNotFound) m := fmt.Sprintf("Resource %v not found", fn) - b, err := errors.Marshal(errors.SabredavNotFound, m, "") + b, err := errors.Marshal(http.StatusNotFound, m, "") errors.HandleWebdavError(&sublog, w, b, err) } errors.HandleErrorStatus(&sublog, w, statRes.Status) @@ -232,7 +232,7 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) m := fmt.Sprintf("Permission denied to remove properties on resource %v", ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(&log, w, b, err) return nil, nil, false } @@ -266,7 +266,7 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) m := fmt.Sprintf("Permission denied to set properties on resource %v", ref.Path) - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(&log, w, b, err) return nil, nil, false } diff --git a/internal/http/services/owncloud/ocdav/props/props.go b/internal/http/services/owncloud/ocdav/props/props.go index 54142cbd979..f3065e357ce 100644 --- a/internal/http/services/owncloud/ocdav/props/props.go +++ b/internal/http/services/owncloud/ocdav/props/props.go @@ -97,3 +97,31 @@ func Next(d *xml.Decoder) (xml.Token, error) { } } } + +// ActiveLock holds active lock xml data +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock +// +type ActiveLock struct { + XMLName xml.Name `xml:"activelock"` + Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"` + Shared *struct{} `xml:"lockscope>shared,omitempty"` + Write *struct{} `xml:"locktype>write,omitempty"` + Depth string `xml:"depth"` + Owner Owner `xml:"owner,omitempty"` + Timeout string `xml:"timeout,omitempty"` + Locktoken string `xml:"locktoken>href"` + Lockroot string `xml:"lockroot>href,omitempty"` +} + +// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner +type Owner struct { + InnerXML string `xml:",innerxml"` +} + +// Escape repaces ", &, ', < and > with their xml representation +func Escape(s string) string { + b := bytes.NewBuffer(nil) + _ = xml.EscapeText(b, []byte(s)) + return b.String() +} diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index aa3d2c2f2b0..481f4fe27ea 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -248,7 +248,7 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ switch uRes.Status.Code { case rpc.Code_CODE_PERMISSION_DENIED: w.WriteHeader(http.StatusForbidden) - b, err := errors.Marshal(errors.SabredavPermissionDenied, "permission denied: you have no permission to upload content", "") + b, err := errors.Marshal(http.StatusForbidden, "permission denied: you have no permission to upload content", "") errors.HandleWebdavError(&log, w, b, err) case rpc.Code_CODE_NOT_FOUND: w.WriteHeader(http.StatusConflict) @@ -289,7 +289,7 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ } if httpRes.StatusCode == errtypes.StatusChecksumMismatch { w.WriteHeader(http.StatusBadRequest) - b, err := errors.Marshal(errors.SabredavBadRequest, "The computed checksum does not match the one received from the client.", "") + b, err := errors.Marshal(http.StatusBadRequest, "The computed checksum does not match the one received from the client.", "") errors.HandleWebdavError(&log, w, b, err) return } diff --git a/internal/http/services/owncloud/ocdav/spacelookup/spacelookup.go b/internal/http/services/owncloud/ocdav/spacelookup/spacelookup.go index b97b18aee02..9f444505098 100644 --- a/internal/http/services/owncloud/ocdav/spacelookup/spacelookup.go +++ b/internal/http/services/owncloud/ocdav/spacelookup/spacelookup.go @@ -32,8 +32,23 @@ import ( "github.com/cs3org/reva/pkg/utils" ) +// LookupReferenceForPath returns: +// a reference with root and relative path +// the status and error for the lookup +func LookupReferenceForPath(ctx context.Context, client gateway.GatewayAPIClient, path string) (*storageProvider.Reference, *rpc.Status, error) { + space, cs3Status, err := LookUpStorageSpaceForPath(ctx, client, path) + if err != nil || cs3Status.Code != rpc.Code_CODE_OK { + return nil, cs3Status, err + } + spacePath := string(space.Opaque.Map["path"].Value) // FIXME error checks + return &storageProvider.Reference{ + ResourceId: space.Root, + Path: utils.MakeRelativePath(strings.TrimPrefix(path, spacePath)), + }, cs3Status, nil +} + // LookUpStorageSpaceForPath returns: -// th storage spaces responsible for a path +// the storage spaces responsible for a path // the status and error for the lookup func LookUpStorageSpaceForPath(ctx context.Context, client gateway.GatewayAPIClient, path string) (*storageProvider.StorageSpace, *rpc.Status, error) { // TODO add filter to only fetch spaces changed in the last 30 sec? diff --git a/internal/http/services/owncloud/ocdav/spaces.go b/internal/http/services/owncloud/ocdav/spaces.go index e633dc84771..21036c0b1f8 100644 --- a/internal/http/services/owncloud/ocdav/spaces.go +++ b/internal/http/services/owncloud/ocdav/spaces.go @@ -22,7 +22,9 @@ import ( "net/http" "path" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/errors" "github.com/cs3org/reva/internal/http/services/owncloud/ocdav/propfind" + "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp/router" ) @@ -71,9 +73,43 @@ func (h *SpacesHandler) Handler(s *svc) http.Handler { case MethodProppatch: s.handleSpacesProppatch(w, r, spaceID) case MethodLock: - s.handleLock(w, r, spaceID) + log := appctx.GetLogger(r.Context()) + // TODO initialize status with http.StatusBadRequest + // TODO initialize err with errors.ErrUnsupportedMethod + status, err := s.handleSpacesLock(w, r, spaceID) + if err != nil { + log.Error().Err(err).Str("space", spaceID).Msg(err.Error()) + } + if status != 0 { // 0 would mean handleLock already sent the response + w.WriteHeader(status) + if status != http.StatusNoContent { + var b []byte + if b, err = errors.Marshal(status, err.Error(), ""); err == nil { + _, err = w.Write(b) + } + if err != nil { + log.Error().Err(err).Str("space", spaceID).Msg(err.Error()) + } + } + } case MethodUnlock: - s.handleUnlock(w, r, spaceID) + log := appctx.GetLogger(r.Context()) + status, err := s.handleUnlock(w, r, spaceID) + if err != nil { + log.Error().Err(err).Str("space", spaceID).Msg(err.Error()) + } + if status != 0 { // 0 would mean handleUnlock already sent the response + w.WriteHeader(status) + if status != http.StatusNoContent { + var b []byte + if b, err = errors.Marshal(status, err.Error(), ""); err == nil { + _, err = w.Write(b) + } + if err != nil { + log.Error().Err(err).Str("space", spaceID).Msg(err.Error()) + } + } + } case MethodMkcol: s.handleSpacesMkCol(w, r, spaceID) case MethodMove: diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index 6bf159160e3..2e28a9be1a9 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -86,13 +86,13 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { if u.Username != username { log.Debug().Str("username", username).Interface("user", u).Msg("trying to read another users trash") // listing other users trash is forbidden, no auth will change that - b, err := errors.Marshal(errors.SabredavNotAuthenticated, "", "") + w.WriteHeader(http.StatusUnauthorized) + b, err := errors.Marshal(http.StatusUnauthorized, "", "") if err != nil { log.Error().Msgf("error marshaling xml response: %s", b) w.WriteHeader(http.StatusInternalServerError) return } - w.WriteHeader(http.StatusUnauthorized) _, err = w.Write(b) if err != nil { log.Error().Msgf("error writing xml response: %s", b) @@ -533,7 +533,8 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc } if parentStatResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - errors.HandleErrorStatus(&sublog, w, &rpc.Status{Code: rpc.Code_CODE_FAILED_PRECONDITION}) + // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + w.WriteHeader(http.StatusConflict) return } } @@ -546,7 +547,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc sublog.Warn().Str("overwrite", overwrite).Msg("dst already exists") w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4 b, err := errors.Marshal( - errors.SabredavPreconditionFailed, + http.StatusPreconditionFailed, "The destination node already exists, and the overwrite header is set to false", net.HeaderOverwrite, ) @@ -598,7 +599,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - b, err := errors.Marshal(errors.SabredavPermissionDenied, "Permission denied to restore", "") + b, err := errors.Marshal(http.StatusForbidden, "Permission denied to restore", "") errors.HandleWebdavError(&sublog, w, b, err) } errors.HandleErrorStatus(&sublog, w, res.Status) @@ -670,7 +671,7 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, sublog.Debug().Str("path", basePath).Str("key", key).Interface("status", res.Status).Msg("resource not found") w.WriteHeader(http.StatusConflict) m := fmt.Sprintf("path %s not found", basePath) - b, err := errors.Marshal(errors.SabredavConflict, m, "") + b, err := errors.Marshal(http.StatusConflict, m, "") errors.HandleWebdavError(&sublog, w, b, err) case rpc.Code_CODE_PERMISSION_DENIED: w.WriteHeader(http.StatusForbidden) @@ -680,7 +681,7 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, } else { m = "Permission denied to delete" } - b, err := errors.Marshal(errors.SabredavPermissionDenied, m, "") + b, err := errors.Marshal(http.StatusForbidden, m, "") errors.HandleWebdavError(&sublog, w, b, err) default: errors.HandleErrorStatus(&sublog, w, res.Status) diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index 7d48af06d02..104429b926a 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -121,7 +121,7 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http. w.WriteHeader(http.StatusPreconditionFailed) return } - // r.Header.Get("OC-Checksum") + // r.Header.Get(net.HeaderOCChecksum) // TODO must be SHA1, ADLER32 or MD5 ... in capital letters???? // curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef' diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go index 9a2d756d310..2bb53916837 100644 --- a/internal/http/services/owncloud/ocdav/versions.go +++ b/internal/http/services/owncloud/ocdav/versions.go @@ -113,7 +113,7 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED || res.Status.Code == rpc.Code_CODE_NOT_FOUND { w.WriteHeader(http.StatusNotFound) - b, err := errors.Marshal(errors.SabredavNotFound, "Resource not found", "") + b, err := errors.Marshal(http.StatusNotFound, "Resource not found", "") errors.HandleWebdavError(&sublog, w, b, err) return } diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index fcb24b6a175..7b8c15b57fc 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -62,10 +62,11 @@ func (h *WebDavHandler) Handler(s *svc) http.Handler { ns, newPath, err := s.ApplyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path) if err != nil { w.WriteHeader(http.StatusNotFound) - b, err := errors.Marshal(errors.SabredavNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "") + b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "") errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err) } r.URL.Path = newPath + switch r.Method { case MethodPropfind: p := propfind.NewHandler(config.PublicURL, func() (propfind.GatewayClient, error) { @@ -73,9 +74,37 @@ func (h *WebDavHandler) Handler(s *svc) http.Handler { }) p.HandlePathPropfind(w, r, ns) case MethodLock: - s.handleLock(w, r, ns) + log := appctx.GetLogger(r.Context()) + // TODO initialize status with http.StatusBadRequest + // TODO initialize err with errors.ErrUnsupportedMethod + status, err := s.handleLock(w, r, ns) + if status != 0 { // 0 would mean handleLock already sent the response + w.WriteHeader(status) + if status != http.StatusNoContent { + var b []byte + if b, err = errors.Marshal(status, err.Error(), ""); err == nil { + _, err = w.Write(b) + } + } + } + if err != nil { + log.Error().Err(err).Msg(err.Error()) + } case MethodUnlock: - s.handleUnlock(w, r, ns) + log := appctx.GetLogger(r.Context()) + status, err := s.handleUnlock(w, r, ns) + if status != 0 { // 0 would mean handleUnlock already sent the response + w.WriteHeader(status) + if status != http.StatusNoContent { + var b []byte + if b, err = errors.Marshal(status, err.Error(), ""); err == nil { + _, err = w.Write(b) + } + } + } + if err != nil { + log.Error().Err(err).Msg(err.Error()) + } case MethodProppatch: s.handlePathProppatch(w, r, ns) case MethodMkcol: diff --git a/internal/http/services/owncloud/ocdav/unlock.go b/pkg/ctx/lockctx.go similarity index 67% rename from internal/http/services/owncloud/ocdav/unlock.go rename to pkg/ctx/lockctx.go index 3b20189c38a..329b41ee17e 100644 --- a/internal/http/services/owncloud/ocdav/unlock.go +++ b/pkg/ctx/lockctx.go @@ -16,13 +16,19 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package ocdav +package ctx import ( - "net/http" + "context" ) -// TODO(jfd): implement unlock -func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) { - w.WriteHeader(http.StatusNotImplemented) +// ContextGetLockID returns the lock id if set in the given context. +func ContextGetLockID(ctx context.Context) (string, bool) { + u, ok := ctx.Value(lockIDKey).(string) + return u, ok +} + +// ContextSetLockID stores the lock id in the context. +func ContextSetLockID(ctx context.Context, t string) context.Context { + return context.WithValue(ctx, lockIDKey, t) } diff --git a/pkg/ctx/userctx.go b/pkg/ctx/userctx.go index f433481f87f..3c50df1879a 100644 --- a/pkg/ctx/userctx.go +++ b/pkg/ctx/userctx.go @@ -30,6 +30,7 @@ const ( userKey key = iota tokenKey idKey + lockIDKey ) // ContextGetUser returns the user if set in the given context. diff --git a/pkg/errtypes/errtypes.go b/pkg/errtypes/errtypes.go index 4f5ced408f8..14d18f744c5 100644 --- a/pkg/errtypes/errtypes.go +++ b/pkg/errtypes/errtypes.go @@ -23,6 +23,8 @@ package errtypes import ( + "strings" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ) @@ -50,6 +52,27 @@ func (e PermissionDenied) Error() string { return "error: permission denied: " + // IsPermissionDenied implements the IsPermissionDenied interface. func (e PermissionDenied) IsPermissionDenied() {} +// Locked is the error to use when a resource cannot be modified because of a lock. +type Locked string + +func (e Locked) Error() string { return "error: locked by " + string(e) } + +// LockID returns the lock ID that caused this error +func (e Locked) LockID() string { + return string(e) +} + +// IsLocked implements the IsLocked interface. +func (e Locked) IsLocked() {} + +// PreconditionFailed is the error to use when a request fails because a requested etag or lock ID mismatches. +type PreconditionFailed string + +func (e PreconditionFailed) Error() string { return "error: precondition failed: " + string(e) } + +// IsPreconditionFailed implements the IsPreconditionFailed interface. +func (e PreconditionFailed) IsPreconditionFailed() {} + // AlreadyExists is the error to use when a resource something is not found. type AlreadyExists string @@ -168,6 +191,18 @@ type IsPermissionDenied interface { IsPermissionDenied() } +// IsLocked is the interface to implement +// to specify that an resource is locked. +type IsLocked interface { + IsLocked() +} + +// IsPreconditionFailed is the interface to implement +// to specify that a precondition failed. +type IsPreconditionFailed interface { + IsPreconditionFailed() +} + // IsPartialContent is the interface to implement // to specify that the client request has partial data. type IsPartialContent interface { @@ -208,14 +243,20 @@ func NewErrtypeFromStatus(status *rpc.Status) error { case rpc.Code_CODE_UNIMPLEMENTED: return NotSupported(status.Message) case rpc.Code_CODE_PERMISSION_DENIED: + // FIXME add locked status! + if strings.HasPrefix(status.Message, "set lock: error: locked by ") { + return Locked(strings.TrimPrefix(status.Message, "set lock: error: locked by ")) + } return PermissionDenied(status.Message) - // case rpc.Code_CODE_DATA_LOSS: ? - // IsPartialContent - // case rpc.Code_CODE_FAILED_PRECONDITION: ? - // IsChecksumMismatch + // case rpc.Code_CODE_LOCKED: + // return Locked(status.Message) + // case rpc.Code_CODE_DATA_LOSS: ? + // IsPartialContent + case rpc.Code_CODE_FAILED_PRECONDITION: + return PreconditionFailed(status.Message) case rpc.Code_CODE_INSUFFICIENT_STORAGE: return InsufficientStorage(status.Message) - case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE: + case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_OUT_OF_RANGE: return BadRequest(status.Message) default: return InternalError(status.Message) diff --git a/pkg/rgrpc/status/status.go b/pkg/rgrpc/status/status.go index e10d85c3497..9c25f5563c3 100644 --- a/pkg/rgrpc/status/status.go +++ b/pkg/rgrpc/status/status.go @@ -24,6 +24,7 @@ package status import ( "context" "errors" + "net/http" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/cs3org/reva/pkg/errtypes" @@ -85,6 +86,15 @@ func NewPermissionDenied(ctx context.Context, err error, msg string) *rpc.Status } } +// NewFailedPrecondition returns a Status with FAILED_PRECONDITION. +func NewFailedPrecondition(ctx context.Context, err error, msg string) *rpc.Status { + return &rpc.Status{ + Code: rpc.Code_CODE_FAILED_PRECONDITION, + Message: msg, + Trace: getTrace(ctx), + } +} + // NewInsufficientStorage returns a Status with INSUFFICIENT_STORAGE. func NewInsufficientStorage(ctx context.Context, err error, msg string) *rpc.Status { return &rpc.Status{ @@ -133,14 +143,21 @@ func NewConflict(ctx context.Context, err error, msg string) *rpc.Status { func NewStatusFromErrType(ctx context.Context, msg string, err error) *rpc.Status { switch e := err.(type) { case nil: - NewOK(ctx) + return NewOK(ctx) case errtypes.IsNotFound: return NewNotFound(ctx, msg+": "+err.Error()) + case errtypes.AlreadyExists: + return NewAlreadyExists(ctx, err, msg+": "+err.Error()) case errtypes.IsInvalidCredentials: // TODO this maps badly return NewUnauthenticated(ctx, err, msg+": "+err.Error()) case errtypes.PermissionDenied: return NewPermissionDenied(ctx, e, msg+": "+err.Error()) + case errtypes.Locked: + // FIXME a locked error returns the current lockid + return NewPermissionDenied(ctx, e, msg+": "+err.Error()) + case errtypes.PreconditionFailed: + return NewFailedPrecondition(ctx, e, msg+": "+err.Error()) case errtypes.IsNotSupported: return NewUnimplemented(ctx, err, msg+":"+err.Error()) case errtypes.BadRequest: @@ -183,3 +200,36 @@ func getTrace(ctx context.Context) string { span := trace.SpanFromContext(ctx) return span.SpanContext().TraceID().String() } + +// a mapping from the CS3 status codes to http codes +var httpStatusCode = map[rpc.Code]int{ + rpc.Code_CODE_ABORTED: http.StatusConflict, + rpc.Code_CODE_ALREADY_EXISTS: http.StatusConflict, + rpc.Code_CODE_CANCELLED: 499, // Client Closed Request + rpc.Code_CODE_DATA_LOSS: http.StatusInternalServerError, + rpc.Code_CODE_DEADLINE_EXCEEDED: http.StatusGatewayTimeout, + rpc.Code_CODE_FAILED_PRECONDITION: http.StatusPreconditionFailed, + rpc.Code_CODE_INSUFFICIENT_STORAGE: http.StatusInsufficientStorage, + rpc.Code_CODE_INTERNAL: http.StatusInternalServerError, + rpc.Code_CODE_INVALID: http.StatusInternalServerError, + rpc.Code_CODE_INVALID_ARGUMENT: http.StatusBadRequest, + rpc.Code_CODE_NOT_FOUND: http.StatusNotFound, + rpc.Code_CODE_OK: http.StatusOK, + rpc.Code_CODE_OUT_OF_RANGE: http.StatusBadRequest, + rpc.Code_CODE_PERMISSION_DENIED: http.StatusForbidden, + rpc.Code_CODE_REDIRECTION: http.StatusTemporaryRedirect, // or permanent? + rpc.Code_CODE_RESOURCE_EXHAUSTED: http.StatusTooManyRequests, + rpc.Code_CODE_UNAUTHENTICATED: http.StatusUnauthorized, + rpc.Code_CODE_UNAVAILABLE: http.StatusServiceUnavailable, + rpc.Code_CODE_UNIMPLEMENTED: http.StatusNotImplemented, + rpc.Code_CODE_UNKNOWN: http.StatusInternalServerError, +} + +// HTTPStatusFromCode returns an HTTP status code for the rpc code. It returns +// an internal server error (500) if the code is unknown +func HTTPStatusFromCode(code rpc.Code) int { + if s, ok := httpStatusCode[code]; ok { + return s + } + return http.StatusInternalServerError +} diff --git a/pkg/storage/fs/nextcloud/nextcloud.go b/pkg/storage/fs/nextcloud/nextcloud.go index 1750cd968c8..8f38342de14 100644 --- a/pkg/storage/fs/nextcloud/nextcloud.go +++ b/pkg/storage/fs/nextcloud/nextcloud.go @@ -786,7 +786,7 @@ func (nc *StorageDriver) RefreshLock(ctx context.Context, ref *provider.Referenc } // Unlock removes an existing lock from the given reference -func (nc *StorageDriver) Unlock(ctx context.Context, ref *provider.Reference) error { +func (nc *StorageDriver) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { return errtypes.NotSupported("unimplemented") } diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index d0fe1d39cd9..c6a9024a1f5 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -1038,7 +1038,7 @@ func (fs *owncloudsqlfs) RefreshLock(ctx context.Context, ref *provider.Referenc } // Unlock removes an existing lock from the given reference -func (fs *owncloudsqlfs) Unlock(ctx context.Context, ref *provider.Reference) error { +func (fs *owncloudsqlfs) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { return errtypes.NotSupported("unimplemented") } diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index 07ea589267b..e624e285618 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -296,7 +296,7 @@ func (fs *s3FS) RefreshLock(ctx context.Context, ref *provider.Reference, lock * } // Unlock removes an existing lock from the given reference -func (fs *s3FS) Unlock(ctx context.Context, ref *provider.Reference) error { +func (fs *s3FS) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { return errtypes.NotSupported("unimplemented") } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 5c61f53234f..732d88535aa 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -61,7 +61,7 @@ type FS interface { SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error - Unlock(ctx context.Context, ref *provider.Reference) error + Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 686fd8977d5..501d3f77fde 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -311,6 +311,11 @@ func (fs *Decomposedfs) CreateDir(ctx context.Context, ref *provider.Reference) return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } + // check lock + if err := n.CheckLock(ctx); err != nil { + return err + } + // verify child does not exist, yet if n, err = n.Child(ctx, name); err != nil { return @@ -433,6 +438,11 @@ func (fs *Decomposedfs) Move(ctx context.Context, oldRef, newRef *provider.Refer return } + // check lock on target + if err := newNode.CheckLock(ctx); err != nil { + return err + } + return fs.tp.Move(ctx, oldNode, newNode) } @@ -521,6 +531,10 @@ func (fs *Decomposedfs) Delete(ctx context.Context, ref *provider.Reference) (er return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) } + if err := node.CheckLock(ctx); err != nil { + return err + } + return fs.tp.Delete(ctx, node) } @@ -555,20 +569,104 @@ func (fs *Decomposedfs) Download(ctx context.Context, ref *provider.Reference) ( // GetLock returns an existing lock on the given reference func (fs *Decomposedfs) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) { - return nil, errtypes.NotSupported("unimplemented") + node, err := fs.lu.NodeFromResource(ctx, ref) + if err != nil { + return nil, errors.Wrap(err, "Decomposedfs: error resolving ref") + } + + if !node.Exists { + err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + return nil, err + } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileDownload + }) + switch { + case err != nil: + return nil, errtypes.InternalError(err.Error()) + case !ok: + return nil, errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + return node.ReadLock(ctx) } // SetLock puts a lock on the given reference func (fs *Decomposedfs) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { - return errtypes.NotSupported("unimplemented") + node, err := fs.lu.NodeFromResource(ctx, ref) + if err != nil { + return errors.Wrap(err, "Decomposedfs: error resolving ref") + } + + if !node.Exists { + return errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + + return node.SetLock(ctx, lock) } // RefreshLock refreshes an existing lock on the given reference func (fs *Decomposedfs) RefreshLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { - return errtypes.NotSupported("unimplemented") + if lock.LockId == "" { + return errtypes.BadRequest("missing lockid") + } + + node, err := fs.lu.NodeFromResource(ctx, ref) + if err != nil { + return errors.Wrap(err, "Decomposedfs: error resolving ref") + } + + if !node.Exists { + return errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + + return node.RefreshLock(ctx, lock) } // Unlock removes an existing lock from the given reference -func (fs *Decomposedfs) Unlock(ctx context.Context, ref *provider.Reference) error { - return errtypes.NotSupported("unimplemented") +func (fs *Decomposedfs) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { + if lock.LockId == "" { + return errtypes.BadRequest("missing lockid") + } + + node, err := fs.lu.NodeFromResource(ctx, ref) + if err != nil { + return errors.Wrap(err, "Decomposedfs: error resolving ref") + } + + if !node.Exists { + return errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + } + + ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { + return rp.InitiateFileUpload // TODO do we need a dedicated permission? + }) + switch { + case err != nil: + return errtypes.InternalError(err.Error()) + case !ok: + return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) + } + + return node.Unlock(ctx, lock) } diff --git a/pkg/storage/utils/decomposedfs/grants.go b/pkg/storage/utils/decomposedfs/grants.go index fc22859460a..c7404d008da 100644 --- a/pkg/storage/utils/decomposedfs/grants.go +++ b/pkg/storage/utils/decomposedfs/grants.go @@ -141,6 +141,11 @@ func (fs *Decomposedfs) RemoveGrant(ctx context.Context, ref *provider.Reference return errtypes.PermissionDenied(filepath.Join(node.ParentID, node.Name)) } + // check lock + if err := node.CheckLock(ctx); err != nil { + return err + } + var attr string if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { attr = xattrs.GrantGroupAcePrefix + g.Grantee.GetGroupId().OpaqueId diff --git a/pkg/storage/utils/decomposedfs/metadata.go b/pkg/storage/utils/decomposedfs/metadata.go index 658feb56047..f45c9ea216d 100644 --- a/pkg/storage/utils/decomposedfs/metadata.go +++ b/pkg/storage/utils/decomposedfs/metadata.go @@ -60,6 +60,11 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider. return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } + // check lock + if err := n.CheckLock(ctx); err != nil { + return err + } + nodePath := n.InternalPath() errs := []error{} @@ -148,6 +153,11 @@ func (fs *Decomposedfs) UnsetArbitraryMetadata(ctx context.Context, ref *provide return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } + // check lock + if err := n.CheckLock(ctx); err != nil { + return err + } + nodePath := n.InternalPath() errs := []error{} for _, k := range keys { diff --git a/pkg/storage/utils/decomposedfs/node/locks.go b/pkg/storage/utils/decomposedfs/node/locks.go new file mode 100644 index 00000000000..eee9e67a2da --- /dev/null +++ b/pkg/storage/utils/decomposedfs/node/locks.go @@ -0,0 +1,202 @@ +// 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 node + +import ( + "context" + "encoding/json" + "os" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/utils" + "github.com/pkg/errors" +) + +// SetLock sets a lock on the node +func (n *Node) SetLock(ctx context.Context, lock *provider.Lock) error { + // check existing lock + if l, _ := n.ReadLock(ctx); l != nil { + lockID, _ := ctxpkg.ContextGetLockID(ctx) + if l.LockId != lockID { + return errtypes.Locked(l.LockId) + } + err := os.Remove(n.LockFilePath()) + if err != nil { + return err + } + } + + // O_EXCL to make open fail when the file already exists + f, err := os.OpenFile(n.LockFilePath(), os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return errors.Wrap(err, "Decomposedfs: could not create lock file") + } + defer f.Close() + + if err := json.NewEncoder(f).Encode(lock); err != nil { + return errors.Wrap(err, "Decomposedfs: could not write lock file") + } + + return nil +} + +// ReadLock reads the lock id for a node +func (n Node) ReadLock(ctx context.Context) (*provider.Lock, error) { + f, err := os.Open(n.LockFilePath()) + if err != nil { + if os.IsNotExist(err) { + return nil, errtypes.NotFound("no lock found") + } + return nil, errors.Wrap(err, "Decomposedfs: could not open lock file") + } + defer f.Close() + + lock := &provider.Lock{} + if err := json.NewDecoder(f).Decode(lock); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("Decomposedfs: could not decode lock file, ignoring") + return nil, errors.Wrap(err, "Decomposedfs: could not read lock file") + } + return lock, nil +} + +// RefreshLock refreshes the node's lock +func (n *Node) RefreshLock(ctx context.Context, lock *provider.Lock) error { + f, err := os.OpenFile(n.LockFilePath(), os.O_RDWR, os.ModeExclusive) + switch { + case os.IsNotExist(err): + return errtypes.PreconditionFailed("lock does not exist") + case err != nil: + return errors.Wrap(err, "Decomposedfs: could not open lock file") + } + defer f.Close() + + oldLock := &provider.Lock{} + if err := json.NewDecoder(f).Decode(oldLock); err != nil { + return errors.Wrap(err, "Decomposedfs: could not read lock") + } + + // check lock + if oldLock.LockId != lock.LockId { + return errtypes.PreconditionFailed("mismatching lock") + } + + u := ctxpkg.ContextMustGetUser(ctx) + if !utils.UserEqual(oldLock.User, u.Id) { + return errtypes.PermissionDenied("cannot refresh lock of another holder") + } + + if !utils.UserEqual(oldLock.User, lock.GetUser()) { + return errtypes.PermissionDenied("cannot change holder when refreshing a lock") + } + + if err := json.NewEncoder(f).Encode(lock); err != nil { + return errors.Wrap(err, "Decomposedfs: could not write lock file") + } + + return nil +} + +// Unlock unlocks the node +func (n *Node) Unlock(ctx context.Context, lock *provider.Lock) error { + f, err := os.OpenFile(n.LockFilePath(), os.O_RDONLY, os.ModeExclusive) + switch { + case os.IsNotExist(err): + return errtypes.PreconditionFailed("lock does not exist") + case err != nil: + return errors.Wrap(err, "Decomposedfs: could not open lock file") + } + defer f.Close() + + oldLock := &provider.Lock{} + if err := json.NewDecoder(f).Decode(oldLock); err != nil { + return errors.Wrap(err, "Decomposedfs: could not read lock") + } + + // check lock + if lock == nil || (oldLock.LockId != lock.LockId) { + return errtypes.Locked(oldLock.LockId) + } + + u := ctxpkg.ContextMustGetUser(ctx) + if !utils.UserEqual(oldLock.User, u.Id) { + return errtypes.PermissionDenied("mismatching holder") + } + + return os.Remove(f.Name()) +} + +// CheckLock compares the context lock with the node lock +func (n *Node) CheckLock(ctx context.Context) error { + lockID, _ := ctxpkg.ContextGetLockID(ctx) + lock, _ := n.ReadLock(ctx) + if lock != nil { + switch lockID { + case "": + return errtypes.Locked(lock.LockId) // no lockid in request + case lock.LockId: + return nil // ok + default: + return errtypes.PreconditionFailed("mismatching lock") + } + } + if lockID != "" { + return errtypes.PreconditionFailed("not locked") + } + return nil // ok +} + +func readLocksIntoOpaque(ctx context.Context, lockPath string, ri *provider.ResourceInfo) { + f, err := os.Open(lockPath) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("Decomposedfs: could not open lock file") + return + } + defer f.Close() + + lock := &provider.Lock{} + if err := json.NewDecoder(f).Decode(lock); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("Decomposedfs: could not read lock file") + } + + // reencode to ensure valid json + var b []byte + if b, err = json.Marshal(lock); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("Decomposedfs: could not marshal locks") + } + if ri.Opaque == nil { + ri.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{}, + } + } + ri.Opaque.Map["lock"] = &types.OpaqueEntry{ + Decoder: "json", + Value: b, + } + // TODO support advisory locks? +} + +// TODO only exclusive locks for WOPI? or advisory locks? +func (n *Node) hasLocks(ctx context.Context) bool { + _, err := os.Stat(n.LockFilePath()) // FIXME better error checking + return err == nil +} diff --git a/pkg/storage/utils/decomposedfs/node/locks_test.go b/pkg/storage/utils/decomposedfs/node/locks_test.go new file mode 100644 index 00000000000..4512964113c --- /dev/null +++ b/pkg/storage/utils/decomposedfs/node/locks_test.go @@ -0,0 +1,218 @@ +// 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 node_test + +import ( + "context" + "os" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/node" + helpers "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/testhelpers" +) + +var _ = Describe("Node locks", func() { + var ( + env *helpers.TestEnv + + lock *provider.Lock + wrongLock *provider.Lock + n *node.Node + n2 *node.Node + + otherUser = &userpb.User{ + Id: &userpb.UserId{ + Idp: "idp", + OpaqueId: "foo", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "foo", + } + otherCtx = ctxpkg.ContextSetUser(context.Background(), otherUser) + ) + + BeforeEach(func() { + var err error + env, err = helpers.NewTestEnv() + Expect(err).ToNot(HaveOccurred()) + + lock = &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: env.Owner.Id, + LockId: uuid.New().String(), + } + wrongLock = &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: env.Owner.Id, + LockId: uuid.New().String(), + } + n = node.New("tobelockedid", "", "tobelocked", 10, "", env.Owner.Id, env.Lookup) + n2 = node.New("neverlockedid", "", "neverlocked", 10, "", env.Owner.Id, env.Lookup) + }) + + AfterEach(func() { + if env != nil { + env.Cleanup() + } + }) + + Describe("SetLock", func() { + It("sets the lock", func() { + _, err := os.Stat(n.LockFilePath()) + Expect(err).To(HaveOccurred()) + + err = n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + _, err = os.Stat(n.LockFilePath()) + Expect(err).ToNot(HaveOccurred()) + }) + + It("refuses to lock if already locked an existing lock was not provided", func() { + err := n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + err = n.SetLock(env.Ctx, lock) + Expect(err).To(HaveOccurred()) + + env.Ctx = ctxpkg.ContextSetLockID(env.Ctx, wrongLock.LockId) + err = n.SetLock(env.Ctx, lock) + Expect(err).To(HaveOccurred()) + }) + + It("relocks if the existing lock was provided", func() { + err := n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + env.Ctx = ctxpkg.ContextSetLockID(env.Ctx, lock.LockId) + err = n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("with an existing lock", func() { + BeforeEach(func() { + err := n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + }) + + Describe("ReadLock", func() { + It("returns the lock", func() { + l, err := n.ReadLock(env.Ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(l).To(Equal(lock)) + }) + + It("reporst an error when the node wasn't locked", func() { + _, err := n2.ReadLock(env.Ctx) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no lock found")) + }) + }) + + Describe("RefreshLock", func() { + var ( + newLock *provider.Lock + ) + + JustBeforeEach(func() { + newLock = &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: env.Owner.Id, + LockId: lock.LockId, + } + }) + + It("fails when the node is unlocked", func() { + err := n2.RefreshLock(env.Ctx, lock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("precondition failed")) + }) + + It("refuses to refresh the lock without holding the lock", func() { + newLock.LockId = "somethingsomething" + err := n.RefreshLock(env.Ctx, newLock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("mismatching")) + }) + + It("refuses to refresh the lock for other users than the lock holder", func() { + err := n.RefreshLock(otherCtx, newLock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("permission denied")) + }) + + It("refuses to change the lock holder", func() { + newLock.User = otherUser.Id + err := n.RefreshLock(env.Ctx, newLock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("permission denied")) + }) + + It("refreshes the lock", func() { + err := n.RefreshLock(env.Ctx, newLock) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe("Unlock", func() { + It("refuses to unlock without having a lock", func() { + err := n.Unlock(env.Ctx, nil) + Expect(err.Error()).To(ContainSubstring(lock.LockId)) + }) + + It("refuses to unlock without having the proper lock", func() { + err := n.Unlock(env.Ctx, nil) + Expect(err.Error()).To(ContainSubstring(lock.LockId)) + + err = n.Unlock(env.Ctx, wrongLock) + Expect(err.Error()).To(ContainSubstring(lock.LockId)) + }) + + It("refuses to unlock for others even if they have the lock", func() { + err := n.Unlock(otherCtx, lock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("mismatching")) + }) + + It("unlocks when the owner uses the lock", func() { + err := n.Unlock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + _, err = os.Stat(n.LockFilePath()) + Expect(err).To(HaveOccurred()) + }) + + It("fails to unlock an unlocked node", func() { + err := n.Unlock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + err = n.Unlock(env.Ctx, lock) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("lock does not exist")) + }) + }) + }) +}) diff --git a/pkg/storage/utils/decomposedfs/node/node.go b/pkg/storage/utils/decomposedfs/node/node.go index e464158485c..0794a217079 100644 --- a/pkg/storage/utils/decomposedfs/node/node.go +++ b/pkg/storage/utils/decomposedfs/node/node.go @@ -33,10 +33,6 @@ import ( "syscall" "time" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/pkg/xattr" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" @@ -48,15 +44,19 @@ import ( "github.com/cs3org/reva/pkg/storage/utils/ace" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" "github.com/cs3org/reva/pkg/utils" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/pkg/xattr" ) // Define keys and values used in the node metadata const ( - FavoriteKey = "http://owncloud.org/ns/favorite" - ShareTypesKey = "http://owncloud.org/ns/share-types" - ChecksumsKey = "http://owncloud.org/ns/checksums" - UserShareType = "0" - QuotaKey = "quota" + LockdiscoveryKey = "DAV:lockdiscovery" + FavoriteKey = "http://owncloud.org/ns/favorite" + ShareTypesKey = "http://owncloud.org/ns/share-types" + ChecksumsKey = "http://owncloud.org/ns/checksums" + UserShareType = "0" + QuotaKey = "quota" QuotaUncalculated = "-1" QuotaUnknown = "-2" @@ -266,7 +266,7 @@ func (n *Node) Child(ctx context.Context, name string) (*Node, error) { return c, nil // if the file does not exist we return a node that has Exists = false } - return nil, errors.Wrap(err, "Decomposedfs: Wrap: readlink error") + return nil, errors.Wrap(err, "decomposedfs: Wrap: readlink error") } c, err := ReadNode(ctx, n.lu, spaceID, filepath.Base(link)) @@ -282,7 +282,7 @@ func (n *Node) Child(ctx context.Context, name string) (*Node, error) { // Parent returns the parent node func (n *Node) Parent() (p *Node, err error) { if n.ParentID == "" { - return nil, fmt.Errorf("Decomposedfs: root has no parent") + return nil, fmt.Errorf("decomposedfs: root has no parent") } var spaceID string if n.SpaceRoot != nil && n.SpaceRoot.ID != n.ParentID { @@ -408,6 +408,11 @@ func (n *Node) ParentInternalPath() string { return n.lu.InternalPath(spaceID, n.ParentID) } +// LockFilePath returns the internal path of the lock file of the node +func (n *Node) LockFilePath() string { + return n.lu.InternalPath(n.ID) + ".lock" +} + // CalculateEtag returns a hash of fileid + tmtime (or mtime) func CalculateEtag(nodeID string, tmTime time.Time) (string, error) { return calculateEtag(nodeID, tmTime) @@ -626,6 +631,12 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi } metadata[FavoriteKey] = favorite } + // read locks + if _, ok := mdKeysMap[LockdiscoveryKey]; returnAllKeys || ok { + if n.hasLocks(ctx) { + readLocksIntoOpaque(ctx, n.LockFilePath(), ri) + } + } // share indicator if _, ok := mdKeysMap[ShareTypesKey]; returnAllKeys || ok { @@ -635,7 +646,7 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi } // checksums - if _, ok := mdKeysMap[ChecksumsKey]; (nodeType == provider.ResourceType_RESOURCE_TYPE_FILE) && returnAllKeys || ok { + if _, ok := mdKeysMap[ChecksumsKey]; (nodeType == provider.ResourceType_RESOURCE_TYPE_FILE) && (returnAllKeys || ok) { // TODO which checksum was requested? sha1 adler32 or md5? for now hardcode sha1? readChecksumIntoResourceChecksum(ctx, nodePath, storageprovider.XSSHA1, ri) readChecksumIntoOpaque(ctx, nodePath, storageprovider.XSMD5, ri) diff --git a/pkg/storage/utils/decomposedfs/node/node_test.go b/pkg/storage/utils/decomposedfs/node/node_test.go index 4517ad352eb..11c0ac9c48a 100644 --- a/pkg/storage/utils/decomposedfs/node/node_test.go +++ b/pkg/storage/utils/decomposedfs/node/node_test.go @@ -19,6 +19,7 @@ package node_test import ( + "encoding/json" "time" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -208,6 +209,27 @@ var _ = Describe("Node", func() { Expect(len(ri.Etag)).To(Equal(34)) Expect(ri.Etag).ToNot(Equal(before)) }) + + It("includes the lock in the Opaque", func() { + lock := &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: env.Owner.Id, + LockId: "foo", + } + err := n.SetLock(env.Ctx, lock) + Expect(err).ToNot(HaveOccurred()) + + perms := node.OwnerPermissions() + ri, err := n.AsResourceInfo(env.Ctx, &perms, []string{}, false) + Expect(err).ToNot(HaveOccurred()) + Expect(ri.Opaque).ToNot(BeNil()) + Expect(ri.Opaque.Map["lock"]).ToNot(BeNil()) + + storedLock := &provider.Lock{} + err = json.Unmarshal(ri.Opaque.Map["lock"].Value, storedLock) + Expect(err).ToNot(HaveOccurred()) + Expect(storedLock).To(Equal(lock)) + }) }) }) }) diff --git a/pkg/storage/utils/decomposedfs/node/permissions.go b/pkg/storage/utils/decomposedfs/node/permissions.go index 7694c4ec071..4622045278c 100644 --- a/pkg/storage/utils/decomposedfs/node/permissions.go +++ b/pkg/storage/utils/decomposedfs/node/permissions.go @@ -99,14 +99,14 @@ func NewPermissions(lu PathLookup) *Permissions { func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap provider.ResourcePermissions, err error) { u, ok := ctxpkg.ContextGetUser(ctx) if !ok { - appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") + appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("no user in context, returning default permissions") return NoPermissions(), nil } // check if the current user is the owner o, err := n.Owner() if err != nil { // TODO check if a parent folder has the owner set? - appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not determine owner, returning default permissions") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n.ID).Msg("could not determine owner, returning default permissions") return NoPermissions(), err } if o.OpaqueId == "" { @@ -119,7 +119,7 @@ func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap prov if err == nil && lp == n.lu.ShareFolder() { return ShareFolderPermissions(), nil } - appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") + appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("user is owner, returning owner permissions") return OwnerPermissions(), nil } // determine root @@ -144,7 +144,7 @@ func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap prov if np, err := cn.ReadUserPermissions(ctx, u); err == nil { AddPermissions(&ap, &np) } else { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn).Msg("error reading permissions") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn.ID).Msg("error reading permissions") // continue with next segment } if cn, err = cn.Parent(); err != nil { @@ -152,7 +152,7 @@ func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap prov } } - appctx.GetLogger(ctx).Debug().Interface("permissions", ap).Interface("node", n).Interface("user", u).Msg("returning agregated permissions") + appctx.GetLogger(ctx).Debug().Interface("permissions", ap).Interface("node", n.ID).Interface("user", u).Msg("returning agregated permissions") return ap, nil } @@ -221,7 +221,7 @@ func nodeHasPermission(ctx context.Context, cn *Node, groupsMap map[string]bool, var grantees []string var err error if grantees, err = cn.ListGrantees(ctx); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn).Msg("error listing grantees") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn.ID).Msg("error listing grantees") return false } @@ -248,14 +248,14 @@ func nodeHasPermission(ctx context.Context, cn *Node, groupsMap map[string]bool, switch { case err == nil: - appctx.GetLogger(ctx).Debug().Interface("node", cn).Str("grant", grantees[i]).Interface("permissions", g.GetPermissions()).Msg("checking permissions") + appctx.GetLogger(ctx).Debug().Interface("node", cn.ID).Str("grant", grantees[i]).Interface("permissions", g.GetPermissions()).Msg("checking permissions") if check(g.GetPermissions()) { return true } case isAttrUnset(err): - appctx.GetLogger(ctx).Error().Interface("node", cn).Str("grant", grantees[i]).Interface("grantees", grantees).Msg("grant vanished from node after listing") + appctx.GetLogger(ctx).Error().Interface("node", cn.ID).Str("grant", grantees[i]).Interface("grantees", grantees).Msg("grant vanished from node after listing") default: - appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn).Str("grant", grantees[i]).Msg("error reading permissions") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", cn.ID).Str("grant", grantees[i]).Msg("error reading permissions") return false } } @@ -266,14 +266,14 @@ func nodeHasPermission(ctx context.Context, cn *Node, groupsMap map[string]bool, func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*userv1beta1.User, *provider.ResourcePermissions) { u, ok := ctxpkg.ContextGetUser(ctx) if !ok { - appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") + appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("no user in context, returning default permissions") perms := NoPermissions() return nil, &perms } // check if the current user is the owner o, err := n.Owner() if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not determine owner, returning default permissions") + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n.ID).Msg("could not determine owner, returning default permissions") perms := NoPermissions() return nil, &perms } @@ -284,7 +284,7 @@ func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*user return nil, &perms } if utils.UserEqual(u.Id, o) { - appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") + appctx.GetLogger(ctx).Debug().Interface("node", n.ID).Msg("user is owner, returning owner permissions") perms := OwnerPermissions() return u, &perms } diff --git a/pkg/storage/utils/decomposedfs/revisions.go b/pkg/storage/utils/decomposedfs/revisions.go index 3c81f52b270..f72efd0f132 100644 --- a/pkg/storage/utils/decomposedfs/revisions.go +++ b/pkg/storage/utils/decomposedfs/revisions.go @@ -167,6 +167,11 @@ func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Refer return errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } + // check lock + if err := n.CheckLock(ctx); err != nil { + return err + } + // move current version to new revision nodePath := fs.lu.InternalPath(spaceID, kp[0]) var fi os.FileInfo diff --git a/pkg/storage/utils/decomposedfs/tree/tree.go b/pkg/storage/utils/decomposedfs/tree/tree.go index a1c0b275835..f75d6c8e890 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree.go +++ b/pkg/storage/utils/decomposedfs/tree/tree.go @@ -440,6 +440,9 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { return } + // Remove lock file if it exists + _ = os.Remove(n.LockFilePath()) + // finally remove the entry from the parent dir src := filepath.Join(n.ParentInternalPath(), n.Name) err = os.Remove(src) @@ -473,6 +476,10 @@ func (t *Tree) RestoreRecycleItemFunc(ctx context.Context, spaceid, key, trashPa } } + if err := targetNode.CheckLock(ctx); err != nil { + return nil, nil, nil, err + } + parent, err := targetNode.Parent() if err != nil { return nil, nil, nil, err diff --git a/pkg/storage/utils/decomposedfs/tree/tree_test.go b/pkg/storage/utils/decomposedfs/tree/tree_test.go index 001950f0051..6f245254bf4 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree_test.go +++ b/pkg/storage/utils/decomposedfs/tree/tree_test.go @@ -27,6 +27,7 @@ import ( helpers "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/testhelpers" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/tree" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" + "github.com/google/uuid" "github.com/pkg/xattr" "github.com/stretchr/testify/mock" @@ -70,36 +71,61 @@ var _ = Describe("Tree", func() { }) Describe("Delete", func() { - JustBeforeEach(func() { - _, err := os.Stat(n.InternalPath()) - Expect(err).ToNot(HaveOccurred()) + Context("when the file was locked", func() { + JustBeforeEach(func() { + _, err := os.Stat(n.InternalPath()) + Expect(err).ToNot(HaveOccurred()) - Expect(t.Delete(env.Ctx, n)).To(Succeed()) + lock := &provider.Lock{ + Type: provider.LockType_LOCK_TYPE_EXCL, + User: env.Owner.Id, + LockId: uuid.New().String(), + } + Expect(n.SetLock(env.Ctx, lock)).To(Succeed()) + Expect(t.Delete(env.Ctx, n)).To(Succeed()) - _, err = os.Stat(n.InternalPath()) - Expect(err).To(HaveOccurred()) - }) + _, err = os.Stat(n.InternalPath()) + Expect(err).To(HaveOccurred()) + }) - It("moves the file to the trash", func() { - trashPath := path.Join(env.Root, "trash", n.SpaceRoot.ID, n.ID) - _, err := os.Stat(trashPath) - Expect(err).ToNot(HaveOccurred()) + It("also removes the lock file", func() { + _, err := os.Stat(n.LockFilePath()) + Expect(err).To(HaveOccurred()) + }) }) - It("removes the file from its original location", func() { - _, err := os.Stat(n.InternalPath()) - Expect(err).To(HaveOccurred()) - }) + Context("when the file was not locked", func() { + JustBeforeEach(func() { + _, err := os.Stat(n.InternalPath()) + Expect(err).ToNot(HaveOccurred()) - It("sets the trash origin xattr", func() { - trashPath := path.Join(env.Root, "trash", n.SpaceRoot.ID, n.ID) - attr, err := xattr.Get(trashPath, xattrs.TrashOriginAttr) - Expect(err).ToNot(HaveOccurred()) - Expect(string(attr)).To(Equal("/dir1/file1")) - }) + Expect(t.Delete(env.Ctx, n)).To(Succeed()) + + _, err = os.Stat(n.InternalPath()) + Expect(err).To(HaveOccurred()) + }) + + It("moves the file to the trash", func() { + trashPath := path.Join(env.Root, "trash", n.SpaceRoot.ID, n.ID) + _, err := os.Stat(trashPath) + Expect(err).ToNot(HaveOccurred()) + }) - It("does not delete the blob from the blobstore", func() { - env.Blobstore.AssertNotCalled(GinkgoT(), "Delete", mock.AnythingOfType("string")) + It("removes the file from its original location", func() { + _, err := os.Stat(n.InternalPath()) + Expect(err).To(HaveOccurred()) + }) + + It("sets the trash origin xattr", func() { + trashPath := path.Join(env.Root, "trash", n.SpaceRoot.ID, n.ID) + attr, err := xattr.Get(trashPath, xattrs.TrashOriginAttr) + Expect(err).ToNot(HaveOccurred()) + Expect(string(attr)).To(Equal("/dir1/file1")) + }) + + It("does not delete the blob from the blobstore", func() { + env.Blobstore.AssertNotCalled(GinkgoT(), "Delete", mock.AnythingOfType("string")) + }) }) }) diff --git a/pkg/storage/utils/decomposedfs/upload.go b/pkg/storage/utils/decomposedfs/upload.go index ad862808d46..0333f7b96f7 100644 --- a/pkg/storage/utils/decomposedfs/upload.go +++ b/pkg/storage/utils/decomposedfs/upload.go @@ -229,6 +229,11 @@ func (fs *Decomposedfs) NewUpload(ctx context.Context, info tusd.FileInfo) (uplo return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } + // check lock + if err := n.CheckLock(ctx); err != nil { + return nil, err + } + info.ID = uuid.New().String() binPath, err := fs.getUploadPath(ctx, info.ID) @@ -464,6 +469,11 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { ) n.SpaceRoot = node.New(node.NoSpaceID, spaceID, "", "", 0, "", nil, upload.fs.lu) + // check lock + if err := n.CheckLock(ctx); err != nil { + return err + } + _, err = node.CheckQuota(n.SpaceRoot, uint64(fi.Size())) if err != nil { return err diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index c2310e2a936..9fbb6c9f1b6 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -551,7 +551,7 @@ func (fs *eosfs) RefreshLock(ctx context.Context, ref *provider.Reference, lock } // Unlock removes an existing lock from the given reference -func (fs *eosfs) Unlock(ctx context.Context, ref *provider.Reference) error { +func (fs *eosfs) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { return errtypes.NotSupported("unimplemented") } diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index e6d3a19d44b..6bbb58ac635 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -731,7 +731,7 @@ func (fs *localfs) RefreshLock(ctx context.Context, ref *provider.Reference, loc } // Unlock removes an existing lock from the given reference -func (fs *localfs) Unlock(ctx context.Context, ref *provider.Reference) error { +func (fs *localfs) Unlock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error { return errtypes.NotSupported("unimplemented") } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 3a350fa3483..e5370a41b10 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -146,6 +146,15 @@ func LaterTS(t1 *types.Timestamp, t2 *types.Timestamp) *types.Timestamp { return t2 } +// TSNow returns the current UTC timestamp +func TSNow() *types.Timestamp { + t := time.Now().UTC() + return &types.Timestamp{ + Seconds: uint64(t.Unix()), + Nanos: uint32(t.Nanosecond()), + } +} + // ExtractGranteeID returns the ID, user or group, set in the GranteeId object func ExtractGranteeID(grantee *provider.Grantee) (*userpb.UserId, *grouppb.GroupId) { switch t := grantee.Id.(type) { @@ -361,3 +370,29 @@ func GetViewMode(viewMode string) gateway.OpenInAppRequest_ViewMode { return gateway.OpenInAppRequest_VIEW_MODE_INVALID } } + +// AppendPlainToOpaque adds a new key value pair as a plain string on the given opaque and returns it +func AppendPlainToOpaque(o *types.Opaque, key, value string) *types.Opaque { + if o == nil { + o = &types.Opaque{} + } + if o.Map == nil { + o.Map = map[string]*types.OpaqueEntry{} + } + o.Map[key] = &types.OpaqueEntry{ + Decoder: "plain", + Value: []byte(value), + } + return o +} + +// ReadPlainFromOpaque reads a plain string from the given opaque map +func ReadPlainFromOpaque(o *types.Opaque, key string) string { + if o == nil || o.Map == nil { + return "" + } + if e, ok := o.Map[key]; ok && e.Decoder == "plain" { + return string(e.Value) + } + return "" +} diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index 6766ac8f2c4..7d608709a02 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -44,12 +44,6 @@ Synchronization features like etag propagation, setting mtime and locking files - [apiMain/checksums.feature:374](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/checksums.feature#L374) #### [Webdav LOCK operations](https://github.com/owncloud/ocis/issues/1284) -- [apiWebdavLocks/exclusiveLocks.feature:18](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L18) -- [apiWebdavLocks/exclusiveLocks.feature:19](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L19) -- [apiWebdavLocks/exclusiveLocks.feature:20](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L20) -- [apiWebdavLocks/exclusiveLocks.feature:21](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L21) -- [apiWebdavLocks/exclusiveLocks.feature:26](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L26) -- [apiWebdavLocks/exclusiveLocks.feature:27](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L27) - [apiWebdavLocks/exclusiveLocks.feature:43](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L43) - [apiWebdavLocks/exclusiveLocks.feature:44](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L44) - [apiWebdavLocks/exclusiveLocks.feature:45](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L45) @@ -178,6 +172,8 @@ Synchronization features like etag propagation, setting mtime and locking files - [apiWebdavLocks/requestsWithToken.feature:132](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L132) - [apiWebdavLocks/requestsWithToken.feature:108](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L108) - [apiWebdavLocks/requestsWithToken.feature:109](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L109) +- [apiWebdavLocks/requestsWithToken.feature:156](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#156) +- [apiWebdavLocks/requestsWithToken.feature:157](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#157) - [apiWebdavLocks/requestsWithToken.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L162) - [apiWebdavLocks2/resharedSharesToShares.feature:33](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks2/resharedSharesToShares.feature#L33) - [apiWebdavLocks2/resharedSharesToShares.feature:34](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks2/resharedSharesToShares.feature#L34) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index 6457f7e5cbb..6382f216057 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -58,12 +58,6 @@ Synchronization features like etag propagation, setting mtime and locking files - [apiMain/checksums.feature:374](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/checksums.feature#L374) #### [Webdav LOCK operations](https://github.com/owncloud/ocis/issues/1284) -- [apiWebdavLocks/exclusiveLocks.feature:18](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L18) -- [apiWebdavLocks/exclusiveLocks.feature:19](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L19) -- [apiWebdavLocks/exclusiveLocks.feature:20](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L20) -- [apiWebdavLocks/exclusiveLocks.feature:21](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L21) -- [apiWebdavLocks/exclusiveLocks.feature:26](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L26) -- [apiWebdavLocks/exclusiveLocks.feature:27](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L27) - [apiWebdavLocks/exclusiveLocks.feature:43](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L43) - [apiWebdavLocks/exclusiveLocks.feature:44](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L44) - [apiWebdavLocks/exclusiveLocks.feature:45](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/exclusiveLocks.feature#L45) @@ -192,6 +186,8 @@ Synchronization features like etag propagation, setting mtime and locking files - [apiWebdavLocks/requestsWithToken.feature:132](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L132) - [apiWebdavLocks/requestsWithToken.feature:108](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L108) - [apiWebdavLocks/requestsWithToken.feature:109](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L109) +- [apiWebdavLocks/requestsWithToken.feature:156](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#156) +- [apiWebdavLocks/requestsWithToken.feature:157](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#157) - [apiWebdavLocks/requestsWithToken.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks/requestsWithToken.feature#L162) - [apiWebdavLocks2/resharedSharesToShares.feature:33](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks2/resharedSharesToShares.feature#L33) - [apiWebdavLocks2/resharedSharesToShares.feature:34](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavLocks2/resharedSharesToShares.feature#L34) diff --git a/tests/integration/grpc/storageprovider_test.go b/tests/integration/grpc/storageprovider_test.go index 8f9af283f17..978486676e7 100644 --- a/tests/integration/grpc/storageprovider_test.go +++ b/tests/integration/grpc/storageprovider_test.go @@ -32,7 +32,9 @@ import ( "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/ocis" jwt "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/pkg/utils" "github.com/cs3org/reva/tests/helpers" + "github.com/google/uuid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -542,6 +544,110 @@ var _ = Describe("storage providers", func() { }) } + assertLocking := func(provider string) { + var ( + subdirRef = ref(provider, subdirPath) + lock = &storagep.Lock{ + Type: storagep.LockType_LOCK_TYPE_EXCL, + User: user.Id, + LockId: uuid.New().String(), + } + ) + It("locks, gets, refreshes and unlocks a lock", func() { + lockRes, err := serviceClient.SetLock(ctx, &storagep.SetLockRequest{ + Ref: subdirRef, + Lock: lock, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(lockRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + + getRes, err := serviceClient.GetLock(ctx, &storagep.GetLockRequest{ + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(getRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + Expect(getRes.Lock).To(Equal(lock)) + + refreshRes, err := serviceClient.RefreshLock(ctx, &storagep.RefreshLockRequest{ + Ref: subdirRef, + Lock: lock, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(refreshRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + + unlockRes, err := serviceClient.Unlock(ctx, &storagep.UnlockRequest{ + Ref: subdirRef, + Lock: lock, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(unlockRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + }) + + Context("with a locked file", func() { + JustBeforeEach(func() { + lockRes, err := serviceClient.SetLock(ctx, &storagep.SetLockRequest{ + Ref: subdirRef, + Lock: lock, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(lockRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + }) + + It("removes the lock when unlocking", func() { + delRes, err := serviceClient.Delete(ctx, &storagep.DeleteRequest{ + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(delRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_PERMISSION_DENIED)) + + unlockRes, err := serviceClient.Unlock(ctx, &storagep.UnlockRequest{ + Ref: subdirRef, + Lock: lock, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(unlockRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + + delRes, err = serviceClient.Delete(ctx, &storagep.DeleteRequest{ + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(delRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + + }) + + Context("with the owner holding the lock", func() { + It("can initiate an upload", func() { + ulRes, err := serviceClient.InitiateFileUpload(ctx, &storagep.InitiateFileUploadRequest{ + Opaque: utils.AppendPlainToOpaque(nil, "lockid", lock.LockId), + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(ulRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + }) + + It("can delete the file", func() { + delRes, err := serviceClient.Delete(ctx, &storagep.DeleteRequest{ + Opaque: utils.AppendPlainToOpaque(nil, "lockid", lock.LockId), + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(delRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_OK)) + + }) + }) + Context("with the owner not holding the lock", func() { + It("can only delete after unlocking the file", func() { + delRes, err := serviceClient.Delete(ctx, &storagep.DeleteRequest{ + Ref: subdirRef, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(delRes.Status.Code).To(Equal(rpcv1beta1.Code_CODE_PERMISSION_DENIED)) + }) + }) + + }) + } + suite := func(provider string, deps map[string]string) { Describe(provider, func() { BeforeEach(func() { @@ -579,6 +685,11 @@ var _ = Describe("storage providers", func() { assertRecycle(provider) assertReferences(provider) assertMetadata(provider) + if provider == "ocis" { + assertLocking(provider) + } else { + PIt("Locking implementation still pending for provider " + provider) + } }) Context("with an existing file /versioned_file", func() {