diff --git a/changelog/unreleased/share-permissions-and-resource-permissions.md b/changelog/unreleased/share-permissions-and-resource-permissions.md new file mode 100644 index 0000000000..e53911dd45 --- /dev/null +++ b/changelog/unreleased/share-permissions-and-resource-permissions.md @@ -0,0 +1,14 @@ +Enhancement: calculate and expose actual file permission set + +Instead of hardcoding the permissions set for every file and folder to ListContainer:true, CreateContainer:true and always reporting the hardcoded string WCKDNVR for the WebDAV permissions we now aggregate the actual cs3 resource permission set in the storage drivers and correctly map them to ocs permissions and webdav permissions using a common role struct that encapsulates the mapping logic. + +https://github.com/cs3org/reva/pull/1368 +https://github.com/owncloud/ocis/issues/552 +https://github.com/owncloud/ocis/issues/762 +https://github.com/owncloud/ocis/issues/763 +https://github.com/owncloud/ocis/issues/893 +https://github.com/owncloud/ocis/issues/1126 +https://github.com/owncloud/ocis-reva/issues/47 +https://github.com/owncloud/ocis-reva/issues/315 +https://github.com/owncloud/ocis-reva/issues/316 +https://github.com/owncloud/product/issues/270 \ No newline at end of file diff --git a/internal/grpc/services/authprovider/authprovider.go b/internal/grpc/services/authprovider/authprovider.go index c7cf0b03ee..b574c4fc28 100644 --- a/internal/grpc/services/authprovider/authprovider.go +++ b/internal/grpc/services/authprovider/authprovider.go @@ -26,6 +26,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/mitchellh/mapstructure" @@ -108,18 +109,26 @@ func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRe password := req.ClientSecret u, err := s.authmgr.Authenticate(ctx, username, password) - if err != nil { + switch v := err.(type) { + case nil: + log.Info().Msgf("user %s authenticated", u.String()) + return &provider.AuthenticateResponse{ + Status: status.NewOK(ctx), + User: u, + }, nil + case errtypes.InvalidCredentials: + return &provider.AuthenticateResponse{ + Status: status.NewPermissionDenied(ctx, v, "wrong password"), + }, nil + case errtypes.NotFound: + return &provider.AuthenticateResponse{ + Status: status.NewNotFound(ctx, "unknown client id"), + }, nil + default: err = errors.Wrap(err, "authsvc: error in Authenticate") - res := &provider.AuthenticateResponse{ + return &provider.AuthenticateResponse{ Status: status.NewUnauthenticated(ctx, err, "error authenticating user"), - } - return res, nil + }, nil } - log.Info().Msgf("user %s authenticated", u.String()) - res := &provider.AuthenticateResponse{ - Status: status.NewOK(ctx), - User: u, - } - return res, nil } diff --git a/internal/grpc/services/gateway/authprovider.go b/internal/grpc/services/gateway/authprovider.go index 15a3113239..3c300ce64e 100644 --- a/internal/grpc/services/gateway/authprovider.go +++ b/internal/grpc/services/gateway/authprovider.go @@ -20,6 +20,7 @@ package gateway import ( "context" + "fmt" provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1" @@ -53,18 +54,24 @@ func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest ClientSecret: req.ClientSecret, } res, err := c.Authenticate(ctx, authProviderReq) - if err != nil { - log.Err(err).Msgf("gateway: error calling Authenticate for type: %s", req.Type) + switch { + case err != nil: return &gateway.AuthenticateResponse{ - Status: status.NewUnauthenticated(ctx, err, "error authenticating request"), + Status: status.NewInternal(ctx, err, fmt.Sprintf("gateway: error calling Authenticate for type: %s", req.Type)), }, nil - } - - if res.Status.Code != rpc.Code_CODE_OK { + case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + fallthrough + case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: + fallthrough + case res.Status.Code == rpc.Code_CODE_NOT_FOUND: + // normal failures, no need to log + return &gateway.AuthenticateResponse{ + Status: res.Status, + }, nil + case res.Status.Code != rpc.Code_CODE_OK: err := status.NewErrorFromCode(res.Status.Code, "gateway") - log.Err(err).Msgf("error authenticating credentials to auth provider for type: %s", req.Type) return &gateway.AuthenticateResponse{ - Status: status.NewUnauthenticated(ctx, err, ""), + Status: status.NewInternal(ctx, err, fmt.Sprintf("error authenticating credentials to auth provider for type: %s", req.Type)), }, nil } diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 9d96bb4108..6303bb1d3e 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -936,7 +936,7 @@ func (s *svc) UnsetArbitraryMetadata(ctx context.Context, req *provider.UnsetArb c, err := s.find(ctx, req.Ref) if err != nil { return &provider.UnsetArbitraryMetadataResponse{ - Status: status.NewStatusFromErrType(ctx, "SetArbitraryMetadata ref="+req.Ref.String(), err), + Status: status.NewStatusFromErrType(ctx, "UnsetArbitraryMetadata ref="+req.Ref.String(), err), }, nil } @@ -1044,7 +1044,7 @@ func (s *svc) stat(ctx context.Context, req *provider.StatRequest) (*provider.St c, err := s.find(ctx, req.Ref) if err != nil { return &provider.StatResponse{ - Status: status.NewStatusFromErrType(ctx, "SetArbitraryMetadata ref="+req.Ref.String(), err), + Status: status.NewStatusFromErrType(ctx, "stat ref="+req.Ref.String(), err), }, nil } diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index 35ef3960b2..5d5e34a479 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -46,6 +46,8 @@ func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareReq Status: status.NewInternal(ctx, err, "error getting user share provider client"), }, nil } + // TODO the user share manager needs to be able to decide if the current user is allowed to create that share (and not eg. incerase permissions) + // jfd: AFAICT this can only be determined by a storage driver - either the storage provider is queried first or the share manager needs to access the storage using a storage driver res, err := c.CreateShare(ctx, req) if err != nil { return nil, errors.Wrap(err, "gateway: error calling CreateShare") diff --git a/internal/grpc/services/publicshareprovider/publicshareprovider.go b/internal/grpc/services/publicshareprovider/publicshareprovider.go index 8ca0801298..2f8a143deb 100644 --- a/internal/grpc/services/publicshareprovider/publicshareprovider.go +++ b/internal/grpc/services/publicshareprovider/publicshareprovider.go @@ -25,6 +25,7 @@ import ( link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/publicshare/manager/registry" "github.com/cs3org/reva/pkg/rgrpc" @@ -145,18 +146,29 @@ func (s *service) RemovePublicShare(ctx context.Context, req *link.RemovePublicS func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublicShareByTokenRequest) (*link.GetPublicShareByTokenResponse, error) { log := appctx.GetLogger(ctx) - log.Info().Msg("getting public share by token") + log.Debug().Msg("getting public share by token") // there are 2 passes here, and the second request has no password found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), req.GetPassword()) - if err != nil { - return nil, err + switch v := err.(type) { + case nil: + return &link.GetPublicShareByTokenResponse{ + Status: status.NewOK(ctx), + Share: found, + }, nil + case errtypes.InvalidCredentials: + return &link.GetPublicShareByTokenResponse{ + Status: status.NewPermissionDenied(ctx, v, "wrong password"), + }, nil + case errtypes.NotFound: + return &link.GetPublicShareByTokenResponse{ + Status: status.NewNotFound(ctx, "unknown token"), + }, nil + default: + return &link.GetPublicShareByTokenResponse{ + Status: status.NewInternal(ctx, v, "unexpected error"), + }, nil } - - return &link.GetPublicShareByTokenResponse{ - Status: status.NewOK(ctx), - Share: found, - }, nil } func (s *service) GetPublicShare(ctx context.Context, req *link.GetPublicShareRequest) (*link.GetPublicShareResponse, error) { diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index 7fbd345011..f39d76b044 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -20,7 +20,7 @@ package publicstorageprovider import ( "context" - "fmt" + "encoding/json" "path" "strings" @@ -28,6 +28,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" @@ -131,16 +132,19 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia return s.initiateFileDownload(ctx, req) } -func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, string, error) { +func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, string, *link.PublicShare, *rpc.Status, error) { log := appctx.GetLogger(ctx) tkn, relativePath, err := s.unwrap(ctx, ref) if err != nil { - return nil, "", err + return nil, "", nil, nil, err } - originalPath, err := s.pathFromToken(ctx, tkn) - if err != nil { - return nil, "", err + originalPath, ls, st, err := s.resolveToken(ctx, tkn) + switch { + case err != nil: + return nil, "", nil, nil, err + case st != nil: + return nil, "", nil, st, nil } cs3Ref := &provider.Reference{ @@ -149,11 +153,12 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider. log.Debug(). Interface("sourceRef", ref). Interface("cs3Ref", cs3Ref). + Interface("share", ls). Str("tkn", tkn). Str("originalPath", originalPath). Str("relativePath", relativePath). Msg("translatePublicRefToCS3Ref") - return cs3Ref, tkn, nil + return cs3Ref, tkn, ls, nil, nil } // Both, t.dir and tokenPath paths need to be merged: @@ -164,9 +169,18 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider. // end = /einstein/files/public-links/foldera/folderb/ func (s *service) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) { - cs3Ref, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - if err != nil { + cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.InitiateFileDownloadResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileDownload: + return &provider.InitiateFileDownloadResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileDownload permission"), + }, nil } dReq := &provider.InitiateFileDownloadRequest{ Ref: cs3Ref, @@ -207,9 +221,18 @@ func (s *service) initiateFileDownload(ctx context.Context, req *provider.Initia } func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) { - cs3Ref, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - if err != nil { + cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.InitiateFileUploadResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileUpload: + return &provider.InitiateFileUploadResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileUpload permission"), + }, nil } uReq := &provider.InitiateFileUploadRequest{ Ref: cs3Ref, @@ -273,9 +296,18 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta trace.StringAttribute("ref", req.Ref.String()), ) - cs3Ref, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - if err != nil { + cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.CreateContainerResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.CreateContainer: + return &provider.CreateContainerResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant CreateContainer permission"), + }, nil } var res *provider.CreateContainerResponse @@ -303,9 +335,18 @@ func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro trace.StringAttribute("ref", req.Ref.String()), ) - cs3Ref, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - if err != nil { + cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.DeleteResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Delete: + return &provider.DeleteResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant Delete permission"), + }, nil } var res *provider.DeleteResponse @@ -334,14 +375,28 @@ func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provide trace.StringAttribute("destination", req.Destination.String()), ) - cs3RefSource, tknSource, err := s.translatePublicRefToCS3Ref(ctx, req.Source) - if err != nil { + cs3RefSource, tknSource, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Source) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.MoveResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Move: + return &provider.MoveResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant Move permission"), + }, nil } // FIXME: maybe there's a shortcut possible here using the source path - cs3RefDestination, tknDest, err := s.translatePublicRefToCS3Ref(ctx, req.Destination) - if err != nil { + cs3RefDestination, tknDest, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Destination) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.MoveResponse{ + Status: st, + }, nil } if tknSource != tknDest { @@ -381,9 +436,18 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide return nil, err } - originalPath, err := s.pathFromToken(ctx, tkn) - if err != nil { + originalPath, ls, st, err := s.resolveToken(ctx, tkn) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.StatResponse{ + Status: st, + }, nil + case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Stat: + return &provider.StatResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant Stat permission"), + }, nil } var statResponse *provider.StatResponse @@ -403,12 +467,31 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide // prevent leaking internal paths if statResponse.Info != nil { + if err := addShare(statResponse.Info, ls); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("share", ls).Interface("info", statResponse.Info).Msg("error when adding share") + } statResponse.Info.Path = path.Join(s.mountPath, "/", tkn, relativePath) + filterPermissions(statResponse.Info.PermissionSet, ls.GetPermissions().Permissions) } return statResponse, nil } +func addShare(i *provider.ResourceInfo, ls *link.PublicShare) error { + if i.Opaque == nil { + i.Opaque = &typesv1beta1.Opaque{} + } + if i.Opaque.Map == nil { + i.Opaque.Map = map[string]*typesv1beta1.OpaqueEntry{} + } + val, err := json.Marshal(ls) + if err != nil { + return err + } + i.Opaque.Map["link-share"] = &typesv1beta1.OpaqueEntry{Decoder: "json", Value: val} + return nil +} + func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, ss provider.ProviderAPI_ListContainerStreamServer) error { return gstatus.Errorf(codes.Unimplemented, "method not implemented") } @@ -419,9 +502,19 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer return nil, err } - pathFromToken, err := s.pathFromToken(ctx, tkn) - if err != nil { + pathFromToken, ls, st, err := s.resolveToken(ctx, tkn) + switch { + case err != nil: return nil, err + case st != nil: + return &provider.ListContainerResponse{ + Status: st, + }, nil + } + if ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.ListContainer { + return &provider.ListContainerResponse{ + Status: status.NewPermissionDenied(ctx, nil, "share does not grant ListContainer permission"), + }, nil } listContainerR, err := s.gateway.ListContainer( @@ -441,12 +534,34 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer } for i := range listContainerR.Infos { + filterPermissions(listContainerR.Infos[i].PermissionSet, ls.GetPermissions().Permissions) listContainerR.Infos[i].Path = path.Join(s.mountPath, "/", tkn, relativePath, path.Base(listContainerR.Infos[i].Path)) } return listContainerR, nil } +func filterPermissions(l *provider.ResourcePermissions, r *provider.ResourcePermissions) { + l.AddGrant = l.AddGrant && r.AddGrant + l.CreateContainer = l.CreateContainer && r.CreateContainer + l.Delete = l.Delete && r.Delete + l.GetPath = l.GetPath && r.GetPath + l.GetQuota = l.GetQuota && r.GetQuota + l.InitiateFileDownload = l.InitiateFileDownload && r.InitiateFileDownload + l.InitiateFileUpload = l.InitiateFileUpload && r.InitiateFileUpload + l.ListContainer = l.ListContainer && r.ListContainer + l.ListFileVersions = l.ListFileVersions && r.ListFileVersions + l.ListGrants = l.ListGrants && r.ListGrants + l.ListRecycle = l.ListRecycle && r.ListRecycle + l.Move = l.Move && r.Move + l.PurgeRecycle = l.PurgeRecycle && r.PurgeRecycle + l.RemoveGrant = l.RemoveGrant && r.RemoveGrant + l.RestoreFileVersion = l.RestoreFileVersion && r.RestoreFileVersion + l.RestoreRecycleItem = l.RestoreRecycleItem && r.RestoreRecycleItem + l.Stat = l.Stat && r.Stat + l.UpdateGrant = l.UpdateGrant && r.UpdateGrant +} + func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (token string, relativePath string, err error) { if ref.GetId() != nil { return "", "", errors.New("need path based ref: got " + ref.String()) @@ -530,14 +645,14 @@ func (s *service) trimMountPrefix(fn string) (string, error) { if strings.HasPrefix(fn, s.mountPath) { return path.Join("/", strings.TrimPrefix(fn, s.mountPath)), nil } - return "", errors.New(fmt.Sprintf("path=%q does not belong to this storage provider mount path=%q"+fn, s.mountPath)) + return "", errors.Errorf("path=%q does not belong to this storage provider mount path=%q"+fn, s.mountPath) } -// pathFromToken returns the path for the publicly shared resource. -func (s *service) pathFromToken(ctx context.Context, token string) (string, error) { +// resolveToken returns the path and share for the publicly shared resource. +func (s *service) resolveToken(ctx context.Context, token string) (string, *link.PublicShare, *rpc.Status, error) { driver, err := pool.GetGatewayServiceClient(s.conf.GatewayAddr) if err != nil { - return "", err + return "", nil, nil, err } publicShareResponse, err := driver.GetPublicShare( @@ -550,16 +665,22 @@ func (s *service) pathFromToken(ctx context.Context, token string) (string, erro }, }, ) - if err != nil { - return "", err + switch { + case err != nil: + return "", nil, nil, err + case publicShareResponse.Status.Code != rpc.Code_CODE_OK: + return "", nil, publicShareResponse.Status, nil } pathRes, err := s.gateway.GetPath(ctx, &provider.GetPathRequest{ ResourceId: publicShareResponse.GetShare().GetResourceId(), }) - if err != nil { - return "", err + switch { + case err != nil: + return "", nil, nil, err + case pathRes.Status.Code != rpc.Code_CODE_OK: + return "", nil, pathRes.Status, nil } - return pathRes.Path, nil + return pathRes.Path, publicShareResponse.GetShare(), nil, nil } diff --git a/internal/http/services/ocmd/shares.go b/internal/http/services/ocmd/shares.go index 108886a4d9..a382e49c0f 100644 --- a/internal/http/services/ocmd/shares.go +++ b/internal/http/services/ocmd/shares.go @@ -29,7 +29,6 @@ import ( ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/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/ocs/conversions" "github.com/cs3org/reva/pkg/appctx" @@ -87,7 +86,7 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { providerInfo := ocmprovider.ProviderInfo{ Domain: meshProvider, Services: []*ocmprovider.Service{ - &ocmprovider.Service{ + { Host: clientIP, }, }, @@ -124,7 +123,7 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { } var permissions conversions.Permissions - var role, token string + var token string options, ok := protocolDecoded["options"].(map[string]interface{}) if !ok { WriteError(w, r, APIErrorInvalidParameter, "protocol: webdav token not provided", nil) @@ -137,24 +136,20 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { return } + var role *conversions.Role pval, ok := options["permissions"].(int) if !ok { - role = conversions.RoleViewer + role = conversions.NewViewerRole() } else { permissions, err = conversions.NewPermissions(pval) if err != nil { WriteError(w, r, APIErrorInvalidParameter, err.Error(), nil) return } - role = conversions.Permissions2Role(permissions) + role = conversions.RoleFromOCSPermissions(permissions) } - var resourcePermissions *provider.ResourcePermissions - resourcePermissions, err = conversions.Role2CS3Permissions(role) - if err != nil { - WriteError(w, r, APIErrorInvalidParameter, "unknown role", err) - } - val, err := json.Marshal(resourcePermissions) + val, err := json.Marshal(role.CS3ResourcePermissions()) if err != nil { WriteError(w, r, APIErrorServerError, "could not encode role", nil) return @@ -173,11 +168,11 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { Name: protocolDecoded["name"].(string), Opaque: &types.Opaque{ Map: map[string]*types.OpaqueEntry{ - "permissions": &types.OpaqueEntry{ + "permissions": { Decoder: "json", Value: val, }, - "token": &types.OpaqueEntry{ + "token": { Decoder: "plain", Value: []byte(token), }, diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index aad7fdeb4a..b0fecf3c47 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -95,7 +95,7 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { } if srcStatRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -111,7 +111,7 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { return } if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -144,7 +144,7 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) } else { - handleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&sublog, w, srcStatRes.Status) } return } diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index bae19d7087..c6e95af51c 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -181,13 +181,21 @@ func (h *DavHandler) Handler(s *svc) http.Handler { } res, err := c.Authenticate(r.Context(), &authenticateRequest) - if err != nil { + switch { + case err != nil: w.WriteHeader(http.StatusInternalServerError) return - } - if res.Status.Code == rpcv1beta1.Code_CODE_UNAUTHENTICATED { + case res.Status.Code == rpcv1beta1.Code_CODE_PERMISSION_DENIED: + fallthrough + case res.Status.Code == rpcv1beta1.Code_CODE_UNAUTHENTICATED: w.WriteHeader(http.StatusUnauthorized) return + case res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + w.WriteHeader(http.StatusNotFound) + return + case res.Status.Code != rpcv1beta1.Code_CODE_OK: + w.WriteHeader(http.StatusInternalServerError) + return } ctx = tokenpkg.ContextSetToken(ctx, res.Token) @@ -196,24 +204,26 @@ func (h *DavHandler) Handler(s *svc) http.Handler { r = r.WithContext(ctx) + // the public share manager knew the token, but does the referenced target still exist? sRes, err := getTokenStatInfo(ctx, c, token) - if err != nil { + switch { + case err != nil: log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return - } - if sRes.Status.Code != rpc.Code_CODE_OK { - switch sRes.Status.Code { - case rpc.Code_CODE_NOT_FOUND: - log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found") - w.WriteHeader(http.StatusNotFound) - case rpc.Code_CODE_PERMISSION_DENIED: - log.Debug().Str("token", token).Interface("status", res.Status).Msg("permission denied") - w.WriteHeader(http.StatusForbidden) - default: - log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed") - w.WriteHeader(http.StatusInternalServerError) - } + case sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + fallthrough + case sRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) // log the difference + return + case sRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: + log.Debug().Str("token", token).Interface("status", res.Status).Msg("unauthorized") + w.WriteHeader(http.StatusUnauthorized) + return + case sRes.Status.Code != rpc.Code_CODE_OK: + log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed") + w.WriteHeader(http.StatusInternalServerError) return } log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path") @@ -233,12 +243,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler { } func getTokenStatInfo(ctx context.Context, client gatewayv1beta1.GatewayAPIClient, token string) (*provider.StatResponse, error) { - ns := "/public" - - fn := path.Join(ns, token) - ref := &provider.Reference{ - Spec: &provider.Reference_Path{Path: fn}, - } - req := &provider.StatRequest{Ref: ref} - return client.Stat(ctx, req) + return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ + Spec: &provider.Reference_Path{Path: path.Join("/public", token)}, + }}) } diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index 2fbc5ab0f0..9083ce5db5 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -58,7 +58,7 @@ func (s *svc) handleDelete(w http.ResponseWriter, r *http.Request, ns string) { } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/http/services/owncloud/ocdav/error.go b/internal/http/services/owncloud/ocdav/error.go index 4d97c97e32..561b91ea40 100644 --- a/internal/http/services/owncloud/ocdav/error.go +++ b/internal/http/services/owncloud/ocdav/error.go @@ -54,7 +54,9 @@ func Marshal(e exception) ([]byte, error) { }) } -func handleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) { +// 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") diff --git a/internal/http/services/owncloud/ocdav/get.go b/internal/http/services/owncloud/ocdav/get.go index 35cf8be1a8..484e790354 100644 --- a/internal/http/services/owncloud/ocdav/get.go +++ b/internal/http/services/owncloud/ocdav/get.go @@ -66,7 +66,7 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { } if sRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } @@ -91,7 +91,7 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { } if dRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, dRes.Status) + HandleErrorStatus(&sublog, w, dRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/head.go b/internal/http/services/owncloud/ocdav/head.go index 29ff689fde..c7daae2c51 100644 --- a/internal/http/services/owncloud/ocdav/head.go +++ b/internal/http/services/owncloud/ocdav/head.go @@ -61,7 +61,7 @@ func (s *svc) handleHead(w http.ResponseWriter, r *http.Request, ns string) { } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go index a5cc97f331..96b74edb2a 100644 --- a/internal/http/services/owncloud/ocdav/mkcol.go +++ b/internal/http/services/owncloud/ocdav/mkcol.go @@ -71,7 +71,7 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { if statRes.Status.Code == rpc.Code_CODE_OK { w.WriteHeader(http.StatusMethodNotAllowed) // 405 if it already exists } else { - handleErrorStatus(&sublog, w, statRes.Status) + HandleErrorStatus(&sublog, w, statRes.Status) } return } @@ -90,6 +90,6 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { sublog.Debug().Str("path", fn).Interface("status", statRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) default: - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) } } diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index 75e4d0d5f4..b754314191 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -80,7 +80,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { return } if srcStatRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -96,7 +96,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { return } if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&sublog, w, srcStatRes.Status) return } @@ -120,7 +120,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { } if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, delRes.Status) + HandleErrorStatus(&sublog, w, delRes.Status) return } } else { @@ -142,7 +142,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) } else { - handleErrorStatus(&sublog, w, intStatRes.Status) + HandleErrorStatus(&sublog, w, intStatRes.Status) } return } @@ -164,7 +164,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { } if mRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, mRes.Status) + HandleErrorStatus(&sublog, w, mRes.Status) return } @@ -176,7 +176,7 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { } if dstStatRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, dstStatRes.Status) + HandleErrorStatus(&sublog, w, dstStatRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index d401947a98..b0c17af211 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -21,6 +21,7 @@ package ocdav import ( "bytes" "context" + "encoding/json" "encoding/xml" "fmt" "io" @@ -33,10 +34,13 @@ import ( "go.opencensus.io/trace" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/pkg/appctx" + ctxuser "github.com/cs3org/reva/pkg/user" "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" ) @@ -90,7 +94,7 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } @@ -111,7 +115,7 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } infos = append(infos, res.Infos...) @@ -138,7 +142,7 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) return } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } @@ -261,6 +265,7 @@ func (s *svc) newProp(key, val string) *propertyXML { // ns is the CS3 namespace that needs to be removed from the CS3 path before // prefixing it with the baseURI func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, ns string) (*responseXML, error) { + sublog := appctx.GetLogger(ctx).With().Interface("md", md).Str("ns", ns).Logger() md.Path = strings.TrimPrefix(md.Path, ns) baseURI := ctx.Value(ctxKeyBaseURI).(string) @@ -275,6 +280,29 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide Propstat: []propstatXML{}, } + var ls *link.PublicShare + if md.Opaque != nil && md.Opaque.Map != nil && md.Opaque.Map["link-share"] != nil && md.Opaque.Map["link-share"].Decoder == "json" { + ls = &link.PublicShare{} + err := json.Unmarshal(md.Opaque.Map["link-share"].Value, ls) + if err != nil { + sublog.Error().Err(err).Msg("could not unmarshal link json") + } + } + + role := conversions.RoleFromResourcePermissions(md.PermissionSet) + + isShared := !isCurrentUserOwner(ctx, md.Owner) + var wdp string + if md.PermissionSet != nil { + wdp = role.WebDAVPermissions( + md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, + isShared, + false, + false, + ) + sublog.Debug().Interface("role", role).Str("dav-permissions", wdp).Msg("converted PermissionSet") + } + // when allprops has been requested if pf.Allprop != nil { // return all known properties @@ -299,8 +327,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } if md.PermissionSet != nil { - // TODO(jfd) no longer hardcode permissions - response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:permissions", "WCKDNVR")) + response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:permissions", wdp)) } // always return size @@ -377,19 +404,82 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:id", "")) } case "permissions": // both + // oc:permissions take several char flags to indicate the permissions the user has on this node: + // D = delete + // NV = update (renameable moveable) + // W = update (files only) + // CK = create (folders only) + // S = Shared + // R = Shareable (Reshare) + // M = Mounted + // in contrast, the ocs:share-permissions further down below indicate clients the maximum permissions that can be granted if md.PermissionSet != nil { - // TODO(jfd): properly build permissions string - propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:permissions", "WCKDNVR")) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:permissions", wdp)) } else { propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:permissions", "")) } + case "public-link-permission": // only on a share root node + if ls != nil && md.PermissionSet != nil { + propstatOK.Prop = append( + propstatOK.Prop, + s.newProp("oc:public-link-permission", strconv.FormatUint(uint64(role.OCSPermissions()), 10))) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-permission", "")) + } + case "public-link-item-type": // only on a share root node + if ls != nil { + if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:public-link-item-type", "folder")) + } else { + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:public-link-item-type", "file")) + // redirectref is another option + } + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-item-type", "")) + } + case "public-link-share-datetime": + if ls != nil && ls.Mtime != nil { + t := utils.TSToTime(ls.Mtime).UTC() // TODO or ctime? + shareTimeString := t.Format(time.RFC1123Z) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:public-link-share-datetime", shareTimeString)) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-share-datetime", "")) + } + case "public-link-share-owner": + if ls != nil && ls.Owner != nil { + if isCurrentUserOwner(ctx, ls.Owner) { + u := ctxuser.ContextMustGetUser(ctx) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:public-link-share-owner", u.Username)) + } else { + u, _ := ctxuser.ContextGetUser(ctx) + sublog.Error().Interface("share", ls).Interface("user", u).Msg("the current user in the context should be the owner of a public link share") + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-share-owner", "")) + } + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-share-owner", "")) + } + case "public-link-expiration": + if ls != nil && ls.Expiration != nil { + t := utils.TSToTime(ls.Expiration).UTC() + expireTimeString := t.Format(time.RFC1123Z) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:public-link-expiration", expireTimeString)) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-expiration", "")) + } + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-expiration", "")) case "size": // phoenix only // TODO we cannot find out if md.Size is set or not because ints in go default to 0 // oc:size is also available on folders propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:size", size)) case "owner-id": // phoenix only - if md.Owner != nil && md.Owner.OpaqueId != "" { - propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:owner-id", s.xmlEscaped(md.Owner.OpaqueId))) + if md.Owner != nil { + if isCurrentUserOwner(ctx, md.Owner) { + u := ctxuser.ContextMustGetUser(ctx) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:owner-id", s.xmlEscaped(u.Username))) + } else { + sublog.Debug().Msg("TODO fetch user username") + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-id", "")) + } } else { propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-id", "")) } @@ -428,8 +518,17 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, "")) } case "owner-display-name": // phoenix only - // TODO(jfd): lookup displayname? or let clients do that? They should cache that IMO - fallthrough + if md.Owner != nil { + if isCurrentUserOwner(ctx, md.Owner) { + u := ctxuser.ContextMustGetUser(ctx) + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:owner-display-name", u.DisplayName)) + } else { + sublog.Debug().Msg("TODO fetch user displayname") + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-display-name", "")) + } + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-display-name", "")) + } case "privatelink": // phoenix only // https://phoenix.owncloud.com/f/9 fallthrough @@ -489,9 +588,21 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } case "http://open-collaboration-services.org/ns": switch pf.Prop[i].Local { + // ocs:share-permissions indicate clients the maximum permissions that can be granted: + // 1 = read + // 2 = write (update) + // 4 = create + // 8 = delete + // 16 = share + // shared files can never have the create or delete permission bit set case "share-permissions": if md.PermissionSet != nil { - perms := conversions.Permissions2OCSPermissions(md.PermissionSet) + perms := role.OCSPermissions() + // shared files cant have the create or delete permission set + if md.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + perms &^= conversions.PermissionCreate + perms &^= conversions.PermissionDelete + } propstatOK.Prop = append(propstatOK.Prop, s.newPropNS(pf.Prop[i].Space, pf.Prop[i].Local, strconv.FormatUint(uint64(perms), 10))) } default: @@ -521,6 +632,17 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide return &response, nil } +// a file is only yours if you are the owner +func isCurrentUserOwner(ctx context.Context, owner *userv1beta1.UserId) bool { + contextUser, ok := ctxuser.ContextGetUser(ctx) + if ok && contextUser.Id != nil && owner != nil && + contextUser.Id.Idp == owner.Idp && + contextUser.Id.OpaqueId == owner.OpaqueId { + return true + } + return false +} + type countingReader struct { n int r io.Reader diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index d957881f98..218bcd5c0f 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -78,7 +78,7 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) } if statRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, statRes.Status) + HandleErrorStatus(&sublog, w, statRes.Status) return } @@ -128,7 +128,7 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } removedProps = append(removedProps, propNameXML) @@ -142,7 +142,7 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go index d13931e89f..a1c071c20c 100644 --- a/internal/http/services/owncloud/ocdav/publicfile.go +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -109,7 +109,7 @@ func (s *svc) adjustResourcePathInURL(w http.ResponseWriter, r *http.Request) bo return false } if pathRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, pathRes.Status) + HandleErrorStatus(&sublog, w, pathRes.Status) return false } if path.Base(r.URL.Path) != path.Base(pathRes.Path) { @@ -173,7 +173,7 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s return } if pathRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, pathRes.Status) + HandleErrorStatus(&sublog, w, pathRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index 7fb5fe813b..b545932bd8 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -167,7 +167,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } @@ -220,7 +220,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } if uRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&sublog, w, uRes.Status) return } @@ -286,7 +286,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } if sRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index 1f663f69de..e931bef312 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -157,7 +157,7 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s return } if getHomeRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, getHomeRes.Status) + HandleErrorStatus(&sublog, w, getHomeRes.Status) return } @@ -178,7 +178,7 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s } if getRecycleRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, getHomeRes.Status) + HandleErrorStatus(&sublog, w, getHomeRes.Status) return } @@ -371,7 +371,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc return } if getHomeRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, getHomeRes.Status) + HandleErrorStatus(&sublog, w, getHomeRes.Status) return } @@ -397,7 +397,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } w.WriteHeader(http.StatusNoContent) @@ -425,7 +425,7 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, return } if getHomeRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, getHomeRes.Status) + HandleErrorStatus(&sublog, w, getHomeRes.Status) return } sRes, err := client.Stat(ctx, &provider.StatRequest{ @@ -441,7 +441,7 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, return } if sRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } @@ -472,6 +472,6 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, sublog.Debug().Str("storageid", sRes.Info.Id.StorageId).Str("key", key).Interface("status", res.Status).Msg("resource not found") w.WriteHeader(http.StatusConflict) default: - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) } } diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index ccdbefd1aa..0c71cf1979 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -94,7 +94,7 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } @@ -150,7 +150,7 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { } if uRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&sublog, w, uRes.Status) return } @@ -231,7 +231,7 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - handleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&sublog, w, sRes.Status) return } diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go index e8cff38e85..882babc0f9 100644 --- a/internal/http/services/owncloud/ocdav/versions.go +++ b/internal/http/services/owncloud/ocdav/versions.go @@ -105,7 +105,7 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, return } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } @@ -121,7 +121,7 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, return } if lvRes.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, lvRes.Status) + HandleErrorStatus(&sublog, w, lvRes.Status) return } @@ -212,7 +212,7 @@ func (h *VersionsHandler) doRestore(w http.ResponseWriter, r *http.Request, s *s return } if res.Status.Code != rpc.Code_CODE_OK { - handleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&sublog, w, res.Status) return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/http/services/owncloud/ocs/conversions/main.go b/internal/http/services/owncloud/ocs/conversions/main.go index 4f74d924cf..eee896d080 100644 --- a/internal/http/services/owncloud/ocs/conversions/main.go +++ b/internal/http/services/owncloud/ocs/conversions/main.go @@ -32,7 +32,6 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" publicsharemgr "github.com/cs3org/reva/pkg/publicshare/manager/registry" usermgr "github.com/cs3org/reva/pkg/user/manager/registry" @@ -168,111 +167,6 @@ type MatchValueData struct { ShareWith string `json:"shareWith" xml:"shareWith"` } -// Role2CS3Permissions converts string roles (from the request body) into cs3 permissions -// TODO(refs) consider using a mask instead of booleans here, might reduce all this boilerplate -func Role2CS3Permissions(r string) (*provider.ResourcePermissions, error) { - switch r { - case RoleViewer: - return &provider.ResourcePermissions{ - ListContainer: true, - ListGrants: true, - ListFileVersions: true, - ListRecycle: true, - Stat: true, - GetPath: true, - GetQuota: true, - InitiateFileDownload: true, - }, nil - case RoleEditor: - return &provider.ResourcePermissions{ - ListContainer: true, - ListGrants: true, - ListFileVersions: true, - ListRecycle: true, - Stat: true, - GetPath: true, - GetQuota: true, - InitiateFileDownload: true, - - Move: true, - InitiateFileUpload: true, - RestoreFileVersion: true, - RestoreRecycleItem: true, - CreateContainer: true, - Delete: true, - PurgeRecycle: true, - }, nil - case RoleCoowner: - return &provider.ResourcePermissions{ - ListContainer: true, - ListGrants: true, - ListFileVersions: true, - ListRecycle: true, - Stat: true, - GetPath: true, - GetQuota: true, - InitiateFileDownload: true, - - Move: true, - InitiateFileUpload: true, - RestoreFileVersion: true, - RestoreRecycleItem: true, - CreateContainer: true, - Delete: true, - PurgeRecycle: true, - - AddGrant: true, - RemoveGrant: true, // TODO when are you able to unshare / delete - UpdateGrant: true, - }, nil - default: - return nil, fmt.Errorf("unknown role: %s", r) - } -} - -// AsCS3Permissions returns permission values as cs3api permissions -// TODO sort out mapping, this is just a first guess -// TODO use roles to make this configurable -func AsCS3Permissions(p int, rp *provider.ResourcePermissions) *provider.ResourcePermissions { - if rp == nil { - rp = &provider.ResourcePermissions{} - } - - if p&int(PermissionRead) != 0 { - rp.ListContainer = true - rp.ListGrants = true - rp.ListFileVersions = true - rp.ListRecycle = true - rp.Stat = true - rp.GetPath = true - rp.GetQuota = true - rp.InitiateFileDownload = true - } - if p&int(PermissionWrite) != 0 { - rp.InitiateFileUpload = true - rp.RestoreFileVersion = true - rp.RestoreRecycleItem = true - } - if p&int(PermissionCreate) != 0 { - rp.CreateContainer = true - // FIXME permissions mismatch: double check create vs write file - rp.InitiateFileUpload = true - if p&int(PermissionWrite) != 0 { - rp.Move = true // TODO move only when create and write? - } - } - if p&int(PermissionDelete) != 0 { - rp.Delete = true - rp.PurgeRecycle = true - } - if p&int(PermissionShare) != 0 { - rp.AddGrant = true - rp.RemoveGrant = true // TODO when are you able to unshare / delete - rp.UpdateGrant = true - } - return rp -} - // UserShare2ShareData converts a cs3api user share into shareData data model // TODO(jfd) merge userShare2ShareData with publicShare2ShareData func UserShare2ShareData(ctx context.Context, share *collaboration.Share) (*ShareData, error) { @@ -356,7 +250,7 @@ func UserIDToString(userID *userpb.UserId) string { // UserSharePermissions2OCSPermissions transforms cs3api permissions into OCS Permissions data model func UserSharePermissions2OCSPermissions(sp *collaboration.SharePermissions) Permissions { if sp != nil { - return Permissions2OCSPermissions(sp.GetPermissions()) + return RoleFromResourcePermissions(sp.GetPermissions()).OCSPermissions() } return PermissionInvalid } @@ -381,48 +275,13 @@ func GetPublicShareManager(manager string, m map[string]map[string]interface{}) func publicSharePermissions2OCSPermissions(sp *link.PublicSharePermissions) Permissions { if sp != nil { - return Permissions2OCSPermissions(sp.GetPermissions()) + return RoleFromResourcePermissions(sp.GetPermissions()).OCSPermissions() } return PermissionInvalid } -// TODO sort out mapping, this is just a first guess -// public link permissions to OCS permissions -func Permissions2OCSPermissions(p *provider.ResourcePermissions) Permissions { - permissions := PermissionInvalid - if p != nil { - if p.ListContainer { - permissions += PermissionRead - } - if p.InitiateFileUpload { - permissions += PermissionWrite - } - if p.CreateContainer { - permissions += PermissionCreate - } - if p.Delete { - permissions += PermissionDelete - } - if p.AddGrant { - permissions += PermissionShare - } - } - return permissions -} - // timestamp is assumed to be UTC ... just human readable ... // FIXME and ambiguous / error prone because there is no time zone ... func timestampToExpiration(t *types.Timestamp) string { return time.Unix(int64(t.Seconds), int64(t.Nanos)).UTC().Format("2006-01-02 15:05:05") } - -const ( - // RoleLegacy provides backwards compatibility - RoleLegacy string = "legacy" - // RoleViewer grants non-editor role on a resource - RoleViewer string = "viewer" - // RoleEditor grants editor permission on a resource - RoleEditor string = "editor" - // RoleCoowner grants owner permissions on a resource - RoleCoowner string = "coowner" -) diff --git a/internal/http/services/owncloud/ocs/conversions/permissions.go b/internal/http/services/owncloud/ocs/conversions/permissions.go index 4d8a544009..4d860fb694 100644 --- a/internal/http/services/owncloud/ocs/conversions/permissions.go +++ b/internal/http/services/owncloud/ocs/conversions/permissions.go @@ -18,7 +18,9 @@ package conversions -import "fmt" +import ( + "fmt" +) // Permissions reflects the CRUD permissions used in the OCS sharing API type Permissions uint @@ -58,20 +60,5 @@ func NewPermissions(val int) (Permissions, error) { // Contain tests if the permissions contain another one. func (p Permissions) Contain(other Permissions) bool { - return p&other != 0 -} - -// Permissions2Role performs permission conversions for user and federated shares -func Permissions2Role(p Permissions) string { - role := RoleLegacy - if p.Contain(PermissionRead) { - role = RoleViewer - } - if p.Contain(PermissionWrite) { - role = RoleEditor - } - if p.Contain(PermissionShare) { - role = RoleCoowner - } - return role + return p&other == other } diff --git a/internal/http/services/owncloud/ocs/conversions/permissions_test.go b/internal/http/services/owncloud/ocs/conversions/permissions_test.go index 419ebd85d2..6604b8deb9 100644 --- a/internal/http/services/owncloud/ocs/conversions/permissions_test.go +++ b/internal/http/services/owncloud/ocs/conversions/permissions_test.go @@ -45,7 +45,7 @@ func TestNewPermissionsWithInvalidValueShouldFail(t *testing.T) { } } -func TestContain(t *testing.T) { +func TestContainPermissionAll(t *testing.T) { table := map[int]Permissions{ 1: PermissionRead, 2: PermissionWrite, @@ -55,25 +55,68 @@ func TestContain(t *testing.T) { 31: PermissionAll, } - for key, value := range table { - p, _ := NewPermissions(key) + p, _ := NewPermissions(31) // all permissions should contain all other permissions + for _, value := range table { if !p.Contain(value) { t.Errorf("permissions %d should contain %d", p, value) } } } +func TestContainPermissionRead(t *testing.T) { + table := map[int]Permissions{ + 2: PermissionWrite, + 4: PermissionCreate, + 8: PermissionDelete, + 16: PermissionShare, + 31: PermissionAll, + } + + p, _ := NewPermissions(1) // read permission should not contain any other permissions + if !p.Contain(PermissionRead) { + t.Errorf("permissions %d should contain %d", p, PermissionRead) + } + for _, value := range table { + if p.Contain(value) { + t.Errorf("permissions %d should not contain %d", p, value) + } + } +} + +func TestContainPermissionCustom(t *testing.T) { + table := map[int]Permissions{ + 2: PermissionWrite, + 8: PermissionDelete, + 31: PermissionAll, + } + + p, _ := NewPermissions(21) // read, create & share permission + if !p.Contain(PermissionRead) { + t.Errorf("permissions %d should contain %d", p, PermissionRead) + } + if !p.Contain(PermissionCreate) { + t.Errorf("permissions %d should contain %d", p, PermissionCreate) + } + if !p.Contain(PermissionShare) { + t.Errorf("permissions %d should contain %d", p, PermissionShare) + } + for _, value := range table { + if p.Contain(value) { + t.Errorf("permissions %d should not contain %d", p, value) + } + } +} func TestContainWithMultiplePermissions(t *testing.T) { table := map[int][]Permissions{ - 3: []Permissions{ + 3: { PermissionRead, PermissionWrite, }, - 5: []Permissions{ + 5: { PermissionRead, PermissionCreate, }, - 31: []Permissions{ + 31: { PermissionRead, PermissionWrite, PermissionCreate, @@ -100,16 +143,16 @@ func TestPermissions2Role(t *testing.T) { } table := map[Permissions]string{ - PermissionRead: RoleViewer, - PermissionWrite: RoleEditor, - PermissionShare: RoleCoowner, + PermissionRead: RoleViewer, + PermissionRead | PermissionWrite | PermissionCreate | PermissionDelete: RoleEditor, PermissionAll: RoleCoowner, - PermissionRead | PermissionWrite: RoleEditor, - PermissionWrite | PermissionShare: RoleCoowner, + PermissionWrite: RoleLegacy, + PermissionShare: RoleLegacy, + PermissionWrite | PermissionShare: RoleLegacy, } for permissions, role := range table { - actual := Permissions2Role(permissions) + actual := RoleFromOCSPermissions(permissions).Name checkRole(role, actual) } } diff --git a/internal/http/services/owncloud/ocs/conversions/role.go b/internal/http/services/owncloud/ocs/conversions/role.go new file mode 100644 index 0000000000..3a64b921e0 --- /dev/null +++ b/internal/http/services/owncloud/ocs/conversions/role.go @@ -0,0 +1,408 @@ +// Copyright 2018-2020 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 conversions sits between CS3 type definitions and OCS API Responses +package conversions + +import ( + "fmt" + "strings" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +// Role describes the interface to transform different permission sets into each other +type Role struct { + Name string + cS3ResourcePermissions *provider.ResourcePermissions + ocsPermissions Permissions +} + +const ( + // RoleUnknown is used for unknown roles + RoleUnknown string = "unknown" + // RoleLegacy provides backwards compatibility + RoleLegacy string = "legacy" + // RoleViewer grants non-editor role on a resource + RoleViewer string = "viewer" + // RoleEditor grants editor permission on a resource, including folders + RoleEditor string = "editor" + // RoleFileEditor grants editor permission on a single file + RoleFileEditor string = "file-editor" + // RoleCoowner grants owner permissions on a resource + RoleCoowner string = "coowner" + // RoleUploader FIXME: uploader role with only write permission can use InitiateFileUpload, not anything else + RoleUploader string = "uploader" +) + +// CS3ResourcePermissions for the role +func (r *Role) CS3ResourcePermissions() *provider.ResourcePermissions { + return r.cS3ResourcePermissions +} + +// OCSPermissions for the role +func (r *Role) OCSPermissions() Permissions { + return r.ocsPermissions +} + +// WebDAVPermissions returns the webdav permissions used in propfinds, eg. "WCKDNVR" +/* + from https://github.com/owncloud/core/blob/10715e2b1c85fc3855a38d2b1fe4426b5e3efbad/apps/dav/lib/Files/PublicFiles/SharedNodeTrait.php#L196-L215 + + $p = ''; + if ($node->isDeletable() && $this->checkSharePermissions(Constants::PERMISSION_DELETE)) { + $p .= 'D'; + } + if ($node->isUpdateable() && $this->checkSharePermissions(Constants::PERMISSION_UPDATE)) { + $p .= 'NV'; // Renameable, Moveable + } + if ($node->getType() === \OCP\Files\FileInfo::TYPE_FILE) { + if ($node->isUpdateable() && $this->checkSharePermissions(Constants::PERMISSION_UPDATE)) { + $p .= 'W'; + } + } else { + if ($node->isCreatable() && $this->checkSharePermissions(Constants::PERMISSION_CREATE)) { + $p .= 'CK'; + } + } + +*/ +// D = delete +// NV = update (renameable moveable) +// W = update (files only) +// CK = create (folders only) +// S = Shared +// R = Shareable +// M = Mounted +func (r *Role) WebDAVPermissions(isDir, isShared, isMountpoint, isPublic bool) string { + var b strings.Builder + //b.Grow(7) + if r.ocsPermissions.Contain(PermissionDelete) { + fmt.Fprintf(&b, "D") // TODO oc10 shows received shares as deletable + } + if r.ocsPermissions.Contain(PermissionWrite) { + fmt.Fprintf(&b, "NV") + if !isDir { + fmt.Fprintf(&b, "W") + } + } + if isDir && r.ocsPermissions.Contain(PermissionCreate) { + fmt.Fprintf(&b, "CK") + } + if !isPublic && isShared { + fmt.Fprintf(&b, "S") + } + if r.ocsPermissions.Contain(PermissionShare) { + fmt.Fprintf(&b, "R") + } + if !isPublic && isMountpoint { + fmt.Fprintf(&b, "M") + } + return b.String() +} + +// RoleFromName creates a role from the name +func RoleFromName(name string) *Role { + switch name { + case RoleViewer: + return NewViewerRole() + case RoleEditor: + return NewEditorRole() + case RoleFileEditor: + return NewFileEditorRole() + case RoleCoowner: + return NewCoownerRole() + case RoleUploader: + return NewUploaderRole() + } + return NewUnknownRole() +} + +// NewUnknownRole creates an unknown role +func NewUnknownRole() *Role { + return &Role{ + Name: RoleUnknown, + cS3ResourcePermissions: &provider.ResourcePermissions{}, + ocsPermissions: PermissionInvalid, + } +} + +// NewViewerRole creates a viewer role +func NewViewerRole() *Role { + return &Role{ + Name: RoleViewer, + cS3ResourcePermissions: &provider.ResourcePermissions{ + // read + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + }, + ocsPermissions: PermissionRead, + } +} + +// NewEditorRole creates an editor role +func NewEditorRole() *Role { + return &Role{ + Name: RoleEditor, + cS3ResourcePermissions: &provider.ResourcePermissions{ + // read + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + + // write + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + + // create + CreateContainer: true, + + // delete + Delete: true, + + // not sure where to put these, but they are part of an editor + Move: true, + PurgeRecycle: true, + }, + ocsPermissions: PermissionRead | PermissionCreate | PermissionWrite | PermissionDelete, + } +} + +// NewFileEditorRole creates a file-editor role +func NewFileEditorRole() *Role { + return &Role{ + Name: RoleEditor, + cS3ResourcePermissions: &provider.ResourcePermissions{ + // read + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + + // write + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + }, + ocsPermissions: PermissionRead | PermissionWrite, + } +} + +// NewCoownerRole creates a coowner role +func NewCoownerRole() *Role { + return &Role{ + Name: RoleCoowner, + cS3ResourcePermissions: &provider.ResourcePermissions{ + // read + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + + // write + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + + // create + CreateContainer: true, + + // delete + Delete: true, + + // not sure where to put these, but they are part of an editor + Move: true, + PurgeRecycle: true, + + // grants + AddGrant: true, + UpdateGrant: true, + RemoveGrant: true, + }, + ocsPermissions: PermissionAll, + } +} + +// NewUploaderRole creates an uploader role +func NewUploaderRole() *Role { + return &Role{ + Name: RoleViewer, + cS3ResourcePermissions: &provider.ResourcePermissions{ + // he will need to make stat requests + // TODO and List requests + Stat: true, + ListContainer: true, + // read + GetPath: true, + // mkdir + CreateContainer: true, + // upload + InitiateFileUpload: true, + }, + ocsPermissions: PermissionCreate, + } +} + +// RoleFromOCSPermissions tries to map ocs permissions to a role +func RoleFromOCSPermissions(p Permissions) *Role { + if p.Contain(PermissionRead) { + if p.Contain(PermissionWrite) && p.Contain(PermissionCreate) && p.Contain(PermissionDelete) { + if p.Contain(PermissionShare) { + return NewCoownerRole() + } + return NewEditorRole() + } + if p == PermissionRead { + return NewViewerRole() + } + } + if p == PermissionCreate { + return NewUploaderRole() + } + // legacy + return NewLegacyRoleFromOCSPermissions(p) +} + +// NewLegacyRoleFromOCSPermissions tries to map a legacy combination of ocs permissions to cs3 resource permissions as a legacy role +func NewLegacyRoleFromOCSPermissions(p Permissions) *Role { + r := &Role{ + Name: RoleLegacy, // TODO custom role? + ocsPermissions: p, + cS3ResourcePermissions: &provider.ResourcePermissions{}, + } + if p.Contain(PermissionRead) { + r.cS3ResourcePermissions.ListContainer = true + r.cS3ResourcePermissions.ListGrants = true + r.cS3ResourcePermissions.ListFileVersions = true + r.cS3ResourcePermissions.ListRecycle = true + r.cS3ResourcePermissions.Stat = true + r.cS3ResourcePermissions.GetPath = true + r.cS3ResourcePermissions.GetQuota = true + r.cS3ResourcePermissions.InitiateFileDownload = true + } + if p.Contain(PermissionWrite) { + r.cS3ResourcePermissions.InitiateFileUpload = true + r.cS3ResourcePermissions.RestoreFileVersion = true + r.cS3ResourcePermissions.RestoreRecycleItem = true + } + if p.Contain(PermissionCreate) { + r.cS3ResourcePermissions.Stat = true + r.cS3ResourcePermissions.ListContainer = true + r.cS3ResourcePermissions.CreateContainer = true + // FIXME permissions mismatch: double check ocs create vs update file + // - if the file exists the ocs api needs to check update permission, + // - if the file does not exist the ocs api needs to check update permission + r.cS3ResourcePermissions.InitiateFileUpload = true + if p.Contain(PermissionWrite) { + r.cS3ResourcePermissions.Move = true // TODO move only when create and write? + } + } + if p.Contain(PermissionDelete) { + r.cS3ResourcePermissions.Delete = true + r.cS3ResourcePermissions.PurgeRecycle = true + } + if p.Contain(PermissionShare) { + r.cS3ResourcePermissions.AddGrant = true + r.cS3ResourcePermissions.RemoveGrant = true // TODO when are you able to unshare / delete + r.cS3ResourcePermissions.UpdateGrant = true + } + return r +} + +// RoleFromResourcePermissions tries to map cs3 resource permissions to a role +func RoleFromResourcePermissions(rp *provider.ResourcePermissions) *Role { + r := &Role{ + Name: RoleUnknown, + ocsPermissions: PermissionInvalid, + cS3ResourcePermissions: rp, + } + if rp == nil { + return r + } + if rp.ListContainer && + rp.ListGrants && + rp.ListFileVersions && + rp.ListRecycle && + rp.Stat && + rp.GetPath && + rp.GetQuota && + rp.InitiateFileDownload { + r.ocsPermissions |= PermissionRead + } + if rp.InitiateFileUpload && + rp.RestoreFileVersion && + rp.RestoreRecycleItem { + r.ocsPermissions |= PermissionWrite + } + if rp.ListContainer && + rp.Stat && + rp.CreateContainer && + rp.InitiateFileUpload { + r.ocsPermissions |= PermissionCreate + } + if rp.Delete && + rp.PurgeRecycle { + r.ocsPermissions |= PermissionDelete + } + if rp.AddGrant && + rp.RemoveGrant && + rp.UpdateGrant { + r.ocsPermissions |= PermissionShare + } + if r.ocsPermissions.Contain(PermissionRead) { + if r.ocsPermissions.Contain(PermissionWrite) && r.ocsPermissions.Contain(PermissionCreate) && r.ocsPermissions.Contain(PermissionDelete) { + r.Name = RoleEditor + if r.ocsPermissions.Contain(PermissionShare) { + r.Name = RoleCoowner + } + return r // editor or coowner + } + if r.ocsPermissions == PermissionRead { + r.Name = RoleViewer + return r + } + } + if r.ocsPermissions == PermissionCreate { + r.Name = RoleUploader + return r + } + r.Name = RoleLegacy + // at this point other ocs permissions may have been mapped. + // TODO what about even more granular cs3 permissions?, eg. only stat + return r +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go index 86a5b6dd2b..430996df8b 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go @@ -22,7 +22,7 @@ import ( "encoding/json" "fmt" "net/http" - "path" + "strconv" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -38,7 +38,7 @@ import ( "github.com/pkg/errors" ) -func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request) { +func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) { ctx := r.Context() log := appctx.GetLogger(ctx) @@ -47,39 +47,6 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request) response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return } - // prefix the path with the owners home, because ocs share requests are relative to the home dir - // TODO the path actually depends on the configured webdav_namespace - hRes, err := c.GetHome(ctx, &provider.GetHomeRequest{}) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get home request", err) - return - } - - prefix := hRes.GetPath() - - statReq := provider.StatRequest{ - Ref: &provider.Reference{ - Spec: &provider.Reference_Path{ - Path: path.Join(prefix, r.FormValue("path")), // TODO replace path with target - }, - }, - } - - statRes, err := c.Stat(ctx, &statReq) - if err != nil { - log.Debug().Err(err).Str("createShare", "shares").Msg("error on stat call") - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing resource information", fmt.Errorf("error getting resource information")) - return - } - - if statRes.Status.Code != rpc.Code_CODE_OK { - if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "resource not found", fmt.Errorf("error creating share on non-existing resource")) - return - } - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error when querying resource", fmt.Errorf("error when querying resource information while creating share, status %d", statRes.Status.Code)) - return - } err = r.ParseForm() if err != nil { @@ -103,8 +70,17 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request) } } + if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + // Single file shares should never have delete or create permissions + role := conversions.RoleFromResourcePermissions(newPermissions) + permissions := role.OCSPermissions() + permissions &^= conversions.PermissionCreate + permissions &^= conversions.PermissionDelete + newPermissions = conversions.RoleFromOCSPermissions(permissions).CS3ResourcePermissions() + } + req := link.CreatePublicShareRequest{ - ResourceInfo: statRes.GetInfo(), + ResourceInfo: statInfo, Grant: &link.Grant{ Permissions: &link.PublicSharePermissions{ Permissions: newPermissions, @@ -137,8 +113,8 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request) createRes, err := c.CreatePublicShare(ctx, &req) if err != nil { - log.Debug().Err(err).Str("createShare", "shares").Msgf("error creating a public share to resource id: %v", statRes.Info.GetId()) - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error creating public share", fmt.Errorf("error creating a public share to resource id: %v", statRes.Info.GetId())) + log.Debug().Err(err).Str("createShare", "shares").Msgf("error creating a public share to resource id: %v", statInfo.GetId()) + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error creating public share", fmt.Errorf("error creating a public share to resource id: %v", statInfo.GetId())) return } @@ -149,7 +125,7 @@ func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request) } s := conversions.PublicShare2ShareData(createRes.Share, r, h.publicURL) - err = h.addFileInfo(ctx, s, statRes.Info) + err = h.addFileInfo(ctx, s, statInfo) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error enhancing response with share data", err) return @@ -471,3 +447,77 @@ func (h *Handler) removePublicShare(w http.ResponseWriter, r *http.Request, shar response.WriteOCSSuccess(w, r, nil) } + +func ocPublicPermToCs3(permKey int, h *Handler) (*provider.ResourcePermissions, error) { + // TODO refactor this ocPublicPermToRole[permKey] check into a conversions.NewPublicSharePermissions? + // not all permissions are possible for public shares + _, ok := ocPublicPermToRole[permKey] + if !ok { + log.Error().Str("ocPublicPermToCs3", "shares").Int("perm", permKey).Msg("invalid public share permission") + return nil, fmt.Errorf("invalid public share permission: %d", permKey) + } + + perm, err := conversions.NewPermissions(permKey) + if err != nil { + return nil, err + } + + return conversions.RoleFromOCSPermissions(perm).CS3ResourcePermissions(), nil +} + +func permissionFromRequest(r *http.Request, h *Handler) (*provider.ResourcePermissions, error) { + var err error + // phoenix sends: {"permissions": 15}. See ocPublicPermToRole struct for mapping + + permKey := 1 + + // note: "permissions" value has higher priority than "publicUpload" + + // handle legacy "publicUpload" arg that overrides permissions differently depending on the scenario + // https://github.com/owncloud/core/blob/v10.4.0/apps/files_sharing/lib/Controller/Share20OcsController.php#L447 + publicUploadString, ok := r.Form["publicUpload"] + if ok { + publicUploadFlag, err := strconv.ParseBool(publicUploadString[0]) + if err != nil { + log.Error().Err(err).Str("publicUpload", publicUploadString[0]).Msg("could not parse publicUpload argument") + return nil, err + } + + if publicUploadFlag { + // all perms except reshare + permKey = 15 + } + } else { + permissionsString, ok := r.Form["permissions"] + if !ok { + // no permission values given + return nil, nil + } + + permKey, err = strconv.Atoi(permissionsString[0]) + if err != nil { + log.Error().Str("permissionFromRequest", "shares").Msgf("invalid type: %T", permKey) + return nil, fmt.Errorf("invalid type: %T", permKey) + } + } + + p, err := ocPublicPermToCs3(permKey, h) + if err != nil { + return nil, err + } + return p, err +} + +// TODO: add mapping for user share permissions to role + +// Maps oc10 public link permissions to roles +var ocPublicPermToRole = map[int]string{ + // Recipients can view and download contents. + 1: "viewer", + // Recipients can view, download, edit, delete and upload contents + 15: "editor", + // Recipients can upload but existing contents are not revealed + 4: "uploader", + // Recipients can view, download and upload contents + 5: "contributor", +} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go index 341388554f..33fe7df96c 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go @@ -20,7 +20,6 @@ package shares import ( "net/http" - "path" "strconv" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -33,28 +32,17 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" - "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) -func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request) { +func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) { ctx := r.Context() - log := appctx.GetLogger(ctx) c, err := pool.GetGatewayServiceClient(h.gatewayAddr) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) return } - // prefix the path with the owners home, because ocs share requests are relative to the home dir - // TODO the path actually depends on the configured webdav_namespace - hRes, err := c.GetHome(ctx, &provider.GetHomeRequest{}) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get home request", err) - return - } - - prefix := hRes.GetPath() shareWithUser, shareWithProvider := r.FormValue("shareWithUser"), r.FormValue("shareWithProvider") if shareWithUser == "" || shareWithProvider == "" { @@ -82,77 +70,61 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque return } - var permissions conversions.Permissions - var role string + var role *conversions.Role pval := r.FormValue("permissions") if pval == "" { // by default only allow read permissions / assign viewer role - permissions = conversions.PermissionRead - role = conversions.RoleViewer + role = conversions.NewViewerRole() } else { pint, err := strconv.Atoi(pval) if err != nil { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "permissions must be an integer", nil) return } - permissions, err = conversions.NewPermissions(pint) + permissions, err := conversions.NewPermissions(pint) if err != nil { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), nil) return } - role = conversions.Permissions2Role(permissions) + role = conversions.RoleFromOCSPermissions(permissions) } - var resourcePermissions *provider.ResourcePermissions - resourcePermissions, err = h.map2CS3Permissions(role, permissions) - if err != nil { - log.Warn().Err(err).Msg("unknown role, mapping legacy permissions") - resourcePermissions = asCS3Permissions(permissions, nil) - } - - statReq := &provider.StatRequest{ - Ref: &provider.Reference{ - Spec: &provider.Reference_Path{ - Path: path.Join(prefix, r.FormValue("path")), - }, - }, - } - statRes, err := c.Stat(ctx, statReq) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc stat request", err) - return - } - if statRes.Status.Code != rpc.Code_CODE_OK { - if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return - } - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc stat request failed", err) - return + if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + // Single file shares should never have delete or create permissions + permissions := role.OCSPermissions() + permissions &^= conversions.PermissionCreate + permissions &^= conversions.PermissionDelete + role = conversions.RoleFromOCSPermissions(permissions) } createShareReq := &ocm.CreateOCMShareRequest{ Opaque: &types.Opaque{ Map: map[string]*types.OpaqueEntry{ - "permissions": &types.OpaqueEntry{ + /* TODO extend the spec with role names? + "role": { + Decoder: "plain", + Value: []byte(role.Name), + }, + */ + "permissions": { Decoder: "plain", - Value: []byte(strconv.Itoa(int(permissions))), + Value: []byte(strconv.Itoa(int(role.OCSPermissions()))), }, - "name": &types.OpaqueEntry{ + "name": { Decoder: "plain", - Value: []byte(statRes.Info.Path), + Value: []byte(statInfo.Path), }, }, }, - ResourceId: statRes.Info.Id, + ResourceId: statInfo.Id, Grant: &ocm.ShareGrant{ Grantee: &provider.Grantee{ Type: provider.GranteeType_GRANTEE_TYPE_USER, Id: remoteUserRes.RemoteUser.GetId(), }, Permissions: &ocm.SharePermissions{ - Permissions: resourcePermissions, + Permissions: role.CS3ResourcePermissions(), }, }, RecipientMeshProvider: providerInfoResp.ProviderInfo, diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 442b30893e..2bc2a2b4e6 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -21,7 +21,6 @@ package shares import ( "context" "encoding/base64" - "encoding/json" "fmt" "mime" "net/http" @@ -39,6 +38,7 @@ import ( types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/rs/zerolog/log" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/config" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" @@ -149,26 +149,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *Handler) createShare(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() shareType, err := strconv.Atoi(r.FormValue("shareType")) if err != nil { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "shareType must be an integer", nil) return } + // get user permissions on the shared file - switch shareType { - case int(conversions.ShareTypeUser): - h.createUserShare(w, r) - case int(conversions.ShareTypePublicLink): - h.createPublicLinkShare(w, r) - case int(conversions.ShareTypeFederatedCloudShare): - h.createFederatedCloudShare(w, r) - default: - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "unknown share type", nil) - } -} - -func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() c, err := pool.GetGatewayServiceClient(h.gatewayAddr) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) @@ -183,236 +171,110 @@ func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request) { } prefix := hRes.GetPath() - sharepath := r.FormValue("path") - // if user sharing is disabled - if h.gatewayAddr == "" { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "user sharing service not configured", nil) - return - } + fn := path.Join(prefix, r.FormValue("path")) - shareWith := r.FormValue("shareWith") - if shareWith == "" { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil) - return + statReq := provider.StatRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: fn, + }, + }, } - userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ - Claim: "username", - Value: shareWith, - }) + sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + + statRes, err := c.Stat(ctx, &statReq) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) + sublog.Debug().Err(err).Str("createShare", "shares").Msg("error on stat call") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing resource information", fmt.Errorf("error getting resource information")) return } - if userRes.Status.Code != rpc.Code_CODE_OK { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err) + if statRes.Status.Code != rpc.Code_CODE_OK { + ocdav.HandleErrorStatus(&sublog, w, statRes.Status) return } - statRes, err := h.stat(ctx, path.Join(prefix, sharepath)) - if err != nil { - response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, fmt.Sprintf("stat on file %s failed", sharepath), err) + // check user has share permissions + if !conversions.RoleFromResourcePermissions(statRes.Info.PermissionSet).OCSPermissions().Contain(conversions.PermissionShare) { + response.WriteOCSError(w, r, http.StatusNotFound, "No share permission", nil) return } - var permissions conversions.Permissions + switch shareType { + case int(conversions.ShareTypeUser): + // user collaborations default to coowner + if h.validatePermissions(w, r, statRes.Info, conversions.NewCoownerRole().OCSPermissions()) { + h.createUserShare(w, r, statRes.Info) + } + case int(conversions.ShareTypePublicLink): + // public links default to read only + if h.validatePermissions(w, r, statRes.Info, conversions.NewViewerRole().OCSPermissions()) { + h.createPublicLinkShare(w, r, statRes.Info) + } + case int(conversions.ShareTypeFederatedCloudShare): + // federated shares default to read only + if h.validatePermissions(w, r, statRes.Info, conversions.NewViewerRole().OCSPermissions()) { + h.createFederatedCloudShare(w, r, statRes.Info) + } + default: + response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "unknown share type", nil) + } +} + +func (h *Handler) validatePermissions(w http.ResponseWriter, r *http.Request, ri *provider.ResourceInfo, defaultPermissions conversions.Permissions) bool { + + // 1. we start without permissions + var reqPermissions conversions.Permissions + + reqRole := r.FormValue("role") - role := r.FormValue("role") - // 2. if we don't have a role try to map the permissions - if role == "" { + // the share role overrides the requested permissions + if reqRole != "" { + reqPermissions = conversions.RoleFromName(reqRole).OCSPermissions() + } else { + // map requested permissions pval := r.FormValue("permissions") if pval == "" { - // default is all permissions / role coowner - permissions = conversions.PermissionAll - role = conversions.RoleCoowner + // default is read permissions / role viewer + // TODO default link vs user share + //reqPermissions = conversions.NewCoownerRole().OCSPermissions() + reqPermissions = defaultPermissions } else { pint, err := strconv.Atoi(pval) if err != nil { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "permissions must be an integer", nil) - return + return false } - permissions, err = conversions.NewPermissions(pint) + reqPermissions, err = conversions.NewPermissions(pint) if err != nil { if err == conversions.ErrPermissionNotInRange { response.WriteOCSError(w, r, http.StatusNotFound, err.Error(), nil) } else { response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), nil) } - return + return false } - role = conversions.Permissions2Role(permissions) } } - if statRes.Info != nil && statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + if ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { // Single file shares should never have delete or create permissions - permissions &^= conversions.PermissionCreate - permissions &^= conversions.PermissionDelete + reqPermissions &^= conversions.PermissionCreate + reqPermissions &^= conversions.PermissionDelete } - var resourcePermissions *provider.ResourcePermissions - resourcePermissions = asCS3Permissions(permissions, resourcePermissions) - - roleMap := map[string]string{"name": role} - val, err := json.Marshal(roleMap) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not encode role", err) - return + existingPermissions := conversions.RoleFromResourcePermissions(ri.PermissionSet).OCSPermissions() + if !existingPermissions.Contain(reqPermissions) { + response.WriteOCSError(w, r, http.StatusNotFound, "Cannot set the requested share permissions", nil) + return false } - - if statRes.Status.Code != rpc.Code_CODE_OK { - if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return - } - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc stat request failed", err) - return - } - - createShareReq := &collaboration.CreateShareRequest{ - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "role": { - Decoder: "json", - Value: val, - }, - }, - }, - ResourceInfo: statRes.Info, - Grant: &collaboration.ShareGrant{ - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - Id: userRes.User.GetId(), - }, - Permissions: &collaboration.SharePermissions{ - Permissions: resourcePermissions, - }, - }, - } - - createShareResponse, err := c.CreateShare(ctx, createShareReq) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) - return - } - if createShareResponse.Status.Code != rpc.Code_CODE_OK { - if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) - return - } - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) - return - } - s, err := conversions.UserShare2ShareData(ctx, createShareResponse.Share) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) - return - } - err = h.addFileInfo(ctx, s, statRes.Info) - if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) - return - } - h.addDisplaynames(ctx, c, s) - h.mapUserIds(ctx, c, s) - - response.WriteOCSSuccess(w, r, s) -} - -func (h *Handler) stat(ctx context.Context, path string) (*provider.StatResponse, error) { - c, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - return nil, fmt.Errorf("error getting grpc gateway client: %s", err.Error()) - } - statReq := &provider.StatRequest{ - Ref: &provider.Reference{ - Spec: &provider.Reference_Path{ - Path: path, - }, - }, - } - - statRes, err := c.Stat(ctx, statReq) - if err != nil { - return nil, fmt.Errorf("error sending a grpc stat request: %s", err.Error()) - } - return statRes, nil + return true } // PublicShareContextName represent cross boundaries context for the name of the public share type PublicShareContextName string -// TODO sort out mapping, this is just a first guess -// TODO use roles to make this configurable -func asCS3Permissions(p conversions.Permissions, rp *provider.ResourcePermissions) *provider.ResourcePermissions { - if rp == nil { - rp = &provider.ResourcePermissions{} - } - - if p.Contain(conversions.PermissionRead) { - rp.ListContainer = true - rp.ListGrants = true - rp.ListFileVersions = true - rp.ListRecycle = true - rp.Stat = true - rp.GetPath = true - rp.GetQuota = true - rp.InitiateFileDownload = true - } - if p.Contain(conversions.PermissionWrite) { - rp.InitiateFileUpload = true - rp.RestoreFileVersion = true - rp.RestoreRecycleItem = true - } - if p.Contain(conversions.PermissionCreate) { - rp.CreateContainer = true - // FIXME permissions mismatch: double check create vs write file - rp.InitiateFileUpload = true - if p.Contain(conversions.PermissionWrite) { - rp.Move = true // TODO move only when create and write? - } - } - if p.Contain(conversions.PermissionDelete) { - rp.Delete = true - rp.PurgeRecycle = true - } - if p.Contain(conversions.PermissionShare) { - rp.AddGrant = true - rp.RemoveGrant = true // TODO when are you able to unshare / delete - rp.UpdateGrant = true - } - return rp -} - -func (h *Handler) map2CS3Permissions(role string, p conversions.Permissions) (*provider.ResourcePermissions, error) { - // TODO replace usage of this method with asCS3Permissions - rp := &provider.ResourcePermissions{ - ListContainer: p.Contain(conversions.PermissionRead), - ListGrants: p.Contain(conversions.PermissionRead), - ListFileVersions: p.Contain(conversions.PermissionRead), - ListRecycle: p.Contain(conversions.PermissionRead), - Stat: p.Contain(conversions.PermissionRead), - GetPath: p.Contain(conversions.PermissionRead), - GetQuota: p.Contain(conversions.PermissionRead), - InitiateFileDownload: p.Contain(conversions.PermissionRead), - - // FIXME: uploader role with only write permission can use InitiateFileUpload, not anything else - Move: p.Contain(conversions.PermissionWrite), - InitiateFileUpload: p.Contain(conversions.PermissionWrite), - CreateContainer: p.Contain(conversions.PermissionCreate), - Delete: p.Contain(conversions.PermissionDelete), - RestoreFileVersion: p.Contain(conversions.PermissionWrite), - RestoreRecycleItem: p.Contain(conversions.PermissionWrite), - PurgeRecycle: p.Contain(conversions.PermissionDelete), - - AddGrant: p.Contain(conversions.PermissionShare), - RemoveGrant: p.Contain(conversions.PermissionShare), // TODO when are you able to unshare / delete - UpdateGrant: p.Contain(conversions.PermissionShare), - } - return rp, nil -} - func (h *Handler) getShare(w http.ResponseWriter, r *http.Request, shareID string) { var share *conversions.ShareData var resourceID *provider.ResourceId @@ -574,7 +436,7 @@ func (h *Handler) updateShare(w http.ResponseWriter, r *http.Request, shareID st Field: &collaboration.UpdateShareRequest_UpdateField_Permissions{ Permissions: &collaboration.SharePermissions{ // this completely overwrites the permissions for this user - Permissions: asCS3Permissions(permissions, nil), + Permissions: conversions.RoleFromOCSPermissions(permissions).CS3ResourcePermissions(), }, }, }, @@ -1146,81 +1008,3 @@ func parseTimestamp(timestampString string) (*types.Timestamp, error) { Nanos: uint32(final % 1000000000), }, nil } - -func ocPublicPermToCs3(permKey int, h *Handler) (*provider.ResourcePermissions, error) { - role, ok := ocPublicPermToRole[permKey] - if !ok { - log.Error().Str("ocPublicPermToCs3", "shares").Msgf("invalid oC permission: %s", role) - return nil, fmt.Errorf("invalid oC permission: %s", role) - } - - perm, err := conversions.NewPermissions(permKey) - if err != nil { - return nil, err - } - - p, err := h.map2CS3Permissions(role, perm) - if err != nil { - log.Error().Str("permissionFromRequest", "shares").Msgf("role to cs3permission %v", perm) - return nil, fmt.Errorf("role to cs3permission failed: %v", perm) - } - - return p, nil -} - -func permissionFromRequest(r *http.Request, h *Handler) (*provider.ResourcePermissions, error) { - var err error - // phoenix sends: {"permissions": 15}. See ocPublicPermToRole struct for mapping - - permKey := 1 - - // note: "permissions" value has higher priority than "publicUpload" - - // handle legacy "publicUpload" arg that overrides permissions differently depending on the scenario - // https://github.com/owncloud/core/blob/v10.4.0/apps/files_sharing/lib/Controller/Share20OcsController.php#L447 - publicUploadString, ok := r.Form["publicUpload"] - if ok { - publicUploadFlag, err := strconv.ParseBool(publicUploadString[0]) - if err != nil { - log.Error().Err(err).Str("publicUpload", publicUploadString[0]).Msg("could not parse publicUpload argument") - return nil, err - } - - if publicUploadFlag { - // all perms except reshare - permKey = 15 - } - } else { - permissionsString, ok := r.Form["permissions"] - if !ok { - // no permission values given - return nil, nil - } - - permKey, err = strconv.Atoi(permissionsString[0]) - if err != nil { - log.Error().Str("permissionFromRequest", "shares").Msgf("invalid type: %T", permKey) - return nil, fmt.Errorf("invalid type: %T", permKey) - } - } - - p, err := ocPublicPermToCs3(permKey, h) - if err != nil { - return nil, err - } - return p, err -} - -// TODO: add mapping for user share permissions to role - -// Maps oc10 public link permissions to roles -var ocPublicPermToRole = map[int]string{ - // Recipients can view and download contents. - 1: "viewer", - // Recipients can view, download, edit, delete and upload contents - 15: "editor", - // Recipients can upload but existing contents are not revealed - 4: "uploader", - // Recipients can view, download and upload contents - 5: "contributor", -} diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 056cb48561..e802562c40 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -19,11 +19,15 @@ package shares import ( + "encoding/json" "net/http" + "strconv" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/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/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" @@ -31,6 +35,131 @@ import ( "github.com/cs3org/reva/pkg/rgrpc/todo/pool" ) +func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) { + ctx := r.Context() + c, err := pool.GetGatewayServiceClient(h.gatewayAddr) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) + return + } + + var role *conversions.Role + + reqRole := r.FormValue("role") + if reqRole != "" { + // default is all permissions / role coowner + role = conversions.RoleFromName(reqRole) + } else { + // map requested permissions + pval := r.FormValue("permissions") + if pval == "" { + // default is all permissions / role coowner + role = conversions.NewCoownerRole() + } else { + pint, err := strconv.Atoi(pval) + if err != nil { + response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "permissions must be an integer", nil) + return + } + permissions, err := conversions.NewPermissions(pint) + if err != nil { + if err == conversions.ErrPermissionNotInRange { + response.WriteOCSError(w, r, http.StatusNotFound, err.Error(), nil) + } else { + response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, err.Error(), nil) + } + return + } + role = conversions.RoleFromOCSPermissions(permissions) + } + } + + if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + // Single file shares should never have delete or create permissions + permissions := role.OCSPermissions() + permissions &^= conversions.PermissionCreate + permissions &^= conversions.PermissionDelete + // editor should be come a file-editor role + role = conversions.RoleFromOCSPermissions(permissions) + } + + roleMap := map[string]string{"name": role.Name} + val, err := json.Marshal(roleMap) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not encode role", err) + return + } + + shareWith := r.FormValue("shareWith") + if shareWith == "" { + response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil) + return + } + + userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ + Claim: "username", + Value: shareWith, + }) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) + return + } + + if userRes.Status.Code != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err) + return + } + + createShareReq := &collaboration.CreateShareRequest{ + Opaque: &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "role": { + Decoder: "json", + Value: val, + }, + }, + }, + ResourceInfo: statInfo, + Grant: &collaboration.ShareGrant{ + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: userRes.User.GetId(), + }, + Permissions: &collaboration.SharePermissions{ + Permissions: role.CS3ResourcePermissions(), + }, + }, + } + + createShareResponse, err := c.CreateShare(ctx, createShareReq) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create share request", err) + return + } + if createShareResponse.Status.Code != rpc.Code_CODE_OK { + if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { + response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil) + return + } + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create share request failed", err) + return + } + s, err := conversions.UserShare2ShareData(ctx, createShareResponse.Share) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error mapping share data", err) + return + } + err = h.addFileInfo(ctx, s, statInfo) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error adding fileinfo to share", err) + return + } + h.addDisplaynames(ctx, c, s) + h.mapUserIds(ctx, c, s) + + response.WriteOCSSuccess(w, r, s) +} + func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, shareID string) { ctx := r.Context() diff --git a/pkg/auth/manager/publicshares/publicshares.go b/pkg/auth/manager/publicshares/publicshares.go index 244e69877b..ce5a8fcb2e 100644 --- a/pkg/auth/manager/publicshares/publicshares.go +++ b/pkg/auth/manager/publicshares/publicshares.go @@ -23,9 +23,11 @@ import ( user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userprovider "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" @@ -74,8 +76,15 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user Token: token, Password: secret, }) - if err != nil { + switch { + case err != nil: return nil, err + case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND: + return nil, errtypes.NotFound(publicShareResponse.Status.Message) + case publicShareResponse.Status.Code == rpcv1beta1.Code_CODE_PERMISSION_DENIED: + return nil, errtypes.InvalidCredentials(publicShareResponse.Status.Message) + case publicShareResponse.Status.Code != rpcv1beta1.Code_CODE_OK: + return nil, errtypes.InternalError(publicShareResponse.Status.Message) } getUserResponse, err := gwConn.GetUser(ctx, &userprovider.GetUserRequest{ diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index 8248a1eb77..d190c39a61 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -41,6 +41,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/publicshare/manager/registry" "github.com/golang/protobuf/jsonpb" @@ -520,13 +521,13 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token, password str return local, nil } - return nil, errors.New("json: invalid password") + return nil, errtypes.InvalidCredentials("json: invalid password") } return local, nil } } - return nil, fmt.Errorf("share with token: `%v` not found", token) + return nil, errtypes.NotFound(fmt.Sprintf("share with token: `%v` not found", token)) } // randString is a helper to create tokens. It could be a token manager instead. diff --git a/pkg/publicshare/manager/memory/memory.go b/pkg/publicshare/manager/memory/memory.go index 6f3e8f1421..81c664983d 100644 --- a/pkg/publicshare/manager/memory/memory.go +++ b/pkg/publicshare/manager/memory/memory.go @@ -32,6 +32,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/publicshare/manager/registry" ) @@ -215,7 +216,7 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token string, passw if ps, ok := m.shares.Load(token); ok { return ps.(*link.PublicShare), nil } - return nil, errors.New("invalid token") + return nil, errtypes.NotFound("invalid token") } func randString(n int) string { diff --git a/pkg/storage/fs/ocis/lookup.go b/pkg/storage/fs/ocis/lookup.go index 2a189095b1..fdc1166e45 100644 --- a/pkg/storage/fs/ocis/lookup.go +++ b/pkg/storage/fs/ocis/lookup.go @@ -59,6 +59,7 @@ func (lu *Lookup) NodeFromPath(ctx context.Context, fn string) (node *Node, err return } + // TODO collect permissions of the current user on every segment if fn != "/" { node, err = lu.WalkPath(ctx, node, fn, func(ctx context.Context, n *Node) error { log.Debug().Interface("node", n).Msg("NodeFromPath() walk") diff --git a/pkg/storage/fs/ocis/node.go b/pkg/storage/fs/ocis/node.go index d8547aff7f..851597f12e 100644 --- a/pkg/storage/fs/ocis/node.go +++ b/pkg/storage/fs/ocis/node.go @@ -36,6 +36,7 @@ import ( "github.com/cs3org/reva/pkg/mime" "github.com/cs3org/reva/pkg/sdk/common" "github.com/cs3org/reva/pkg/storage/utils/ace" + "github.com/cs3org/reva/pkg/user" "github.com/pkg/errors" "github.com/pkg/xattr" "github.com/rs/zerolog/log" @@ -52,8 +53,7 @@ type Node struct { ParentID string ID string Name string - ownerID string // used to cache the owner id - ownerIDP string // used to cache the owner idp + owner *userpb.UserId Exists bool } @@ -132,13 +132,17 @@ func ReadRecycleItem(ctx context.Context, lu *Lookup, key string) (n *Node, tras } // lookup ownerId in extended attributes if attrBytes, err = xattr.Get(deletedNodePath, ownerIDAttr); err == nil { - n.ownerID = string(attrBytes) + n.owner = &userpb.UserId{} + n.owner.OpaqueId = string(attrBytes) } else { return } // lookup ownerIdp in extended attributes if attrBytes, err = xattr.Get(deletedNodePath, ownerIDPAttr); err == nil { - n.ownerIDP = string(attrBytes) + if n.owner == nil { + n.owner = &userpb.UserId{} + } + n.owner.Idp = string(attrBytes) } else { return } @@ -273,32 +277,60 @@ func (n *Node) Parent() (p *Node, err error) { // Owner returns the cached owner id or reads it from the extended attributes // TODO can be private as only the AsResourceInfo uses it -func (n *Node) Owner() (id string, idp string, err error) { - if n.ownerID != "" && n.ownerIDP != "" { - return n.ownerID, n.ownerIDP, nil +func (n *Node) Owner() (o *userpb.UserId, err error) { + if n.owner != nil { + return n.owner, nil } + // FIXME ... do we return the owner of the reference or the owner of the target? + // we don't really know the owner of the target ... and as the reference may point anywhere we cannot really find out + // but what are the permissions? all? none? the gateway has to fill in? + // TODO what if this is a reference? nodePath := n.lu.toInternalPath(n.ID) // lookup parent id in extended attributes var attrBytes []byte // lookup name in extended attributes if attrBytes, err = xattr.Get(nodePath, ownerIDAttr); err == nil { - n.ownerID = string(attrBytes) + if n.owner == nil { + n.owner = &userpb.UserId{} + } + n.owner.OpaqueId = string(attrBytes) } else { return } // lookup name in extended attributes if attrBytes, err = xattr.Get(nodePath, ownerIDPAttr); err == nil { - n.ownerIDP = string(attrBytes) + if n.owner == nil { + n.owner = &userpb.UserId{} + } + n.owner.Idp = string(attrBytes) } else { return } - return n.ownerID, n.ownerIDP, err + return n.owner, err +} + +// PermissionSet returns the permission set for the current user +// the parent nodes are not taken into account +func (n *Node) PermissionSet(ctx context.Context) *provider.ResourcePermissions { + u, ok := user.ContextGetUser(ctx) + if !ok { + appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") + return noPermissions + } + if o, _ := n.Owner(); isSameUserID(u.Id, o) { + return ownerPermissions + } + // read the permissions for the current user from the acls of the current node + if np, err := n.ReadUserPermissions(ctx, u); err == nil { + return np + } + return noPermissions } // AsResourceInfo return the node as CS3 ResourceInfo -func (n *Node) AsResourceInfo(ctx context.Context, mdKeys []string) (ri *provider.ResourceInfo, err error) { - log := appctx.GetLogger(ctx) +func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissions, mdKeys []string) (ri *provider.ResourceInfo, err error) { + sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger() var fn string nodePath := n.lu.toInternalPath(n.ID) @@ -334,21 +366,17 @@ func (n *Node) AsResourceInfo(ctx context.Context, mdKeys []string) (ri *provide } ri = &provider.ResourceInfo{ - Id: id, - Path: fn, - Type: nodeType, - MimeType: mime.Detect(nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, fn), - Size: uint64(fi.Size()), - // TODO fix permissions - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + Id: id, + Path: fn, + Type: nodeType, + MimeType: mime.Detect(nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, fn), + Size: uint64(fi.Size()), Target: string(target), + PermissionSet: rp, } - if owner, idp, err := n.Owner(); err == nil { - ri.Owner = &userpb.UserId{ - Idp: idp, - OpaqueId: owner, - } + if ri.Owner, err = n.Owner(); err != nil { + sublog.Debug().Err(err).Msg("could not determine owner") } // etag currently is a hash of fileid + tmtime (or mtime) @@ -397,12 +425,12 @@ func (n *Node) AsResourceInfo(ctx context.Context, mdKeys []string) (ri *provide if v, err := xattr.Get(nodePath, attrs[i]); err == nil { ri.ArbitraryMetadata.Metadata[k] = string(v) } else { - log.Error().Err(err).Interface("node", n).Str("attr", attrs[i]).Msg("could not get attribute value") + sublog.Error().Err(err).Str("attr", attrs[i]).Msg("could not get attribute value") } } } } else { - log.Error().Err(err).Interface("node", n).Msg("could not list attributes") + sublog.Error().Err(err).Msg("could not list attributes") } if common.FindString(mdKeys, _shareTypesKey) != -1 { @@ -411,7 +439,7 @@ func (n *Node) AsResourceInfo(ctx context.Context, mdKeys []string) (ri *provide } } - log.Debug(). + sublog.Debug(). Interface("ri", ri). Msg("AsResourceInfo") @@ -452,6 +480,85 @@ func (n *Node) UnsetTempEtag() (err error) { return err } +// ReadUserPermissions will assemble the permissions for the current user on the given node without parent nodes +func (n *Node) ReadUserPermissions(ctx context.Context, u *userpb.User) (ap *provider.ResourcePermissions, err error) { + // 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") + return noPermissions, err + } + if o.OpaqueId == "" { + // this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner + // TODO what if no owner is set but grants are present? + return noOwnerPermissions, nil + } + if isSameUserID(u.Id, o) { + appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") + return ownerPermissions, nil + } + + ap = &provider.ResourcePermissions{} + + // for an efficient group lookup convert the list of groups to a map + // groups are just strings ... groupnames ... or group ids ??? AAARGH !!! + groupsMap := make(map[string]bool, len(u.Groups)) + for i := range u.Groups { + groupsMap[u.Groups[i]] = true + } + + var g *provider.Grant + + // we read all grantees from the node + var grantees []string + if grantees, err = n.ListGrantees(ctx); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("error listing grantees") + return nil, err + } + + // instead of making n getxattr syscalls we are going to list the acls and filter them here + // we have two options here: + // 1. we can start iterating over the acls / grants on the node or + // 2. we can iterate over the number of groups + // The current implementation tries to be defensive for cases where users have hundreds or thousands of groups, so we iterate over the existing acls. + userace := grantPrefix + _userAcePrefix + u.Id.OpaqueId + userFound := false + for i := range grantees { + switch { + // we only need to find the user once + case !userFound && grantees[i] == userace: + g, err = n.ReadGrant(ctx, grantees[i]) + case strings.HasPrefix(grantees[i], grantPrefix+_groupAcePrefix): // only check group grantees + gr := strings.TrimPrefix(grantees[i], grantPrefix+_groupAcePrefix) + if groupsMap[gr] { + g, err = n.ReadGrant(ctx, grantees[i]) + } else { + // no need to check attribute + continue + } + default: + // no need to check attribute + continue + } + + switch { + case err == nil: + addPermissions(ap, g.GetPermissions()) + case isNoData(err): + err = nil + appctx.GetLogger(ctx).Error().Interface("node", n).Str("grant", grantees[i]).Interface("grantees", grantees).Msg("grant vanished from node after listing") + // continue with next segment + default: + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Str("grant", grantees[i]).Msg("error reading permissions") + // continue with next segment + } + } + + appctx.GetLogger(ctx).Debug().Interface("permissions", ap).Interface("node", n).Interface("user", u).Msg("returning aggregated permissions") + return ap, nil +} + // ListGrantees lists the grantees of the current node // We don't want to wast time and memory by creating grantee objects. // The function will return a list of opaque strings that can be used to make a ReadGrant call diff --git a/pkg/storage/fs/ocis/ocis.go b/pkg/storage/fs/ocis/ocis.go index 2cb9d2be10..1c3e211511 100644 --- a/pkg/storage/fs/ocis/ocis.go +++ b/pkg/storage/fs/ocis/ocis.go @@ -26,6 +26,7 @@ import ( "path/filepath" "strings" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" @@ -203,6 +204,15 @@ func (fs *ocisfs) CreateHome(ctx context.Context) (err error) { } return nil }) + if err != nil { + return + } + + // update the owner + u := user.ContextMustGetUser(ctx) + if err = h.writeMetadata(u.Id); err != nil { + return + } if fs.o.TreeTimeAccounting { homePath := h.lu.toInternalPath(h.ID) @@ -361,17 +371,15 @@ func (fs *ocisfs) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []s return } - ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { - return rp.Stat - }) + rp, err := fs.p.AssemblePermissions(ctx, node) switch { case err != nil: return nil, errtypes.InternalError(err.Error()) - case !ok: + case !rp.Stat: return nil, errtypes.PermissionDenied(node.ID) } - return node.AsResourceInfo(ctx, mdKeys) + return node.AsResourceInfo(ctx, rp, mdKeys) } func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) (finfos []*provider.ResourceInfo, err error) { @@ -385,13 +393,11 @@ func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKey return } - ok, err := fs.p.HasPermission(ctx, node, func(rp *provider.ResourcePermissions) bool { - return rp.ListContainer - }) + rp, err := fs.p.AssemblePermissions(ctx, node) switch { case err != nil: return nil, errtypes.InternalError(err.Error()) - case !ok: + case !rp.ListContainer: return nil, errtypes.PermissionDenied(node.ID) } @@ -402,7 +408,10 @@ func (fs *ocisfs) ListFolder(ctx context.Context, ref *provider.Reference, mdKey } for i := range children { - if ri, err := children[i].AsResourceInfo(ctx, mdKeys); err == nil { + np := rp + // add this childs permissions + addPermissions(np, node.PermissionSet(ctx)) + if ri, err := children[i].AsResourceInfo(ctx, np, mdKeys); err == nil { finfos = append(finfos, ri) } } @@ -497,3 +506,14 @@ func (fs *ocisfs) copyMD(s string, t string) (err error) { } return nil } + +func isSameUserID(i *userpb.UserId, j *userpb.UserId) bool { + switch { + case i == nil, j == nil: + return false + case i.OpaqueId == j.OpaqueId && i.Idp == j.Idp: + return true + default: + return false + } +} diff --git a/pkg/storage/fs/ocis/permissions.go b/pkg/storage/fs/ocis/permissions.go index d74c0f4f31..2b77fb93fa 100644 --- a/pkg/storage/fs/ocis/permissions.go +++ b/pkg/storage/fs/ocis/permissions.go @@ -36,7 +36,7 @@ const ( _groupAcePrefix = "g:" ) -var defaultPermissions *provider.ResourcePermissions = &provider.ResourcePermissions{ +var noPermissions *provider.ResourcePermissions = &provider.ResourcePermissions{ // no permissions } @@ -71,6 +71,88 @@ type Permissions struct { lu *Lookup } +// AssemblePermissions will assemble the permissions for the current user on the given node, taking into account all parent nodes +func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap *provider.ResourcePermissions, err error) { + u, ok := user.ContextGetUser(ctx) + if !ok { + appctx.GetLogger(ctx).Debug().Interface("node", n).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") + return noPermissions, err + } + if o.OpaqueId == "" { + // this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner + // TODO what if no owner is set but grants are present? + return noOwnerPermissions, nil + } + if isSameUserID(u.Id, o) { + appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") + return ownerPermissions, nil + } + + // determine root + var rn *Node + if rn, err = p.lu.RootNode(ctx); err != nil { + return nil, err + } + + cn := n + + ap = &provider.ResourcePermissions{} + + // for an efficient group lookup convert the list of groups to a map + // groups are just strings ... groupnames ... or group ids ??? AAARGH !!! + groupsMap := make(map[string]bool, len(u.Groups)) + for i := range u.Groups { + groupsMap[u.Groups[i]] = true + } + + // for all segments, starting at the leaf + for cn.ID != rn.ID { + + 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") + // continue with next segment + } + + if cn, err = cn.Parent(); err != nil { + return ap, errors.Wrap(err, "ocisfs: error getting parent "+cn.ParentID) + } + } + + appctx.GetLogger(ctx).Debug().Interface("permissions", ap).Interface("node", n).Interface("user", u).Msg("returning agregated permissions") + return ap, nil +} + +// TODO we should use a bitfield for this ... +func addPermissions(l *provider.ResourcePermissions, r *provider.ResourcePermissions) { + l.AddGrant = l.AddGrant || r.AddGrant + l.CreateContainer = l.CreateContainer || r.CreateContainer + l.Delete = l.Delete || r.Delete + l.GetPath = l.GetPath || r.GetPath + l.GetQuota = l.GetQuota || r.GetQuota + l.InitiateFileDownload = l.InitiateFileDownload || r.InitiateFileDownload + l.InitiateFileUpload = l.InitiateFileUpload || r.InitiateFileUpload + l.ListContainer = l.ListContainer || r.ListContainer + l.ListFileVersions = l.ListFileVersions || r.ListFileVersions + l.ListGrants = l.ListGrants || r.ListGrants + l.ListRecycle = l.ListRecycle || r.ListRecycle + l.Move = l.Move || r.Move + l.PurgeRecycle = l.PurgeRecycle || r.PurgeRecycle + l.RemoveGrant = l.RemoveGrant || r.RemoveGrant + l.RestoreFileVersion = l.RestoreFileVersion || r.RestoreFileVersion + l.RestoreRecycleItem = l.RestoreRecycleItem || r.RestoreRecycleItem + l.Stat = l.Stat || r.Stat + l.UpdateGrant = l.UpdateGrant || r.UpdateGrant +} + // HasPermission call check() for every node up to the root until check returns true func (p *Permissions) HasPermission(ctx context.Context, n *Node, check func(*provider.ResourcePermissions) bool) (can bool, err error) { @@ -145,7 +227,7 @@ func (p *Permissions) HasPermission(ctx context.Context, n *Node, check func(*pr } } - appctx.GetLogger(ctx).Debug().Interface("permissions", defaultPermissions).Interface("node", n).Interface("user", u).Msg("no grant found, returning default permissions") + appctx.GetLogger(ctx).Debug().Interface("permissions", noPermissions).Interface("node", n).Interface("user", u).Msg("no grant found, returning default permissions") return false, nil } @@ -153,19 +235,20 @@ func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*user u, ok := user.ContextGetUser(ctx) if !ok { appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") - return nil, defaultPermissions + return nil, noPermissions } // check if the current user is the owner - id, _, err := n.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") - return nil, defaultPermissions + return nil, noPermissions } - if id == "" { + if o.OpaqueId == "" { + // this happens for root nodes in the storage. the extended attributes are set to emptystring to indicate: no owner // TODO what if no owner is set but grants are present? return nil, noOwnerPermissions } - if id == u.Id.OpaqueId { + if isSameUserID(u.Id, o) { appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") return u, ownerPermissions } diff --git a/pkg/storage/fs/ocis/recycle.go b/pkg/storage/fs/ocis/recycle.go index 63cef3aa18..577298cf08 100644 --- a/pkg/storage/fs/ocis/recycle.go +++ b/pkg/storage/fs/ocis/recycle.go @@ -50,13 +50,14 @@ func (fs *ocisfs) ListRecycle(ctx context.Context) (items []*provider.RecycleIte items = make([]*provider.RecycleItem, 0) // TODO how do we check if the storage allows listing the recycle for the current user? check owner of the root of the storage? + // use permissions ReadUserPermissions? if fs.o.EnableHome { if !ownerPermissions.ListContainer { log.Debug().Msg("owner not allowed to list trash") return items, errtypes.PermissionDenied("owner not allowed to list trash") } } else { - if !defaultPermissions.ListContainer { + if !noPermissions.ListContainer { log.Debug().Msg("default permissions prevent listing trash") return items, errtypes.PermissionDenied("default permissions prevent listing trash") } diff --git a/pkg/storage/fs/ocis/tree.go b/pkg/storage/fs/ocis/tree.go index 0b3b0c88b2..7024a6448f 100644 --- a/pkg/storage/fs/ocis/tree.go +++ b/pkg/storage/fs/ocis/tree.go @@ -28,7 +28,6 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/user" "github.com/google/uuid" "github.com/pkg/errors" "github.com/pkg/xattr" @@ -94,27 +93,19 @@ func (t *Tree) CreateDir(ctx context.Context, node *Node) (err error) { // create a directory node node.ID = uuid.New().String() - // who will become the owner? - u, ok := user.ContextGetUser(ctx) - switch { - case ok: - // we have a user in context - err = createNode(node, u.Id) - case t.lu.Options.EnableHome: - // enable home requires a user - log := appctx.GetLogger(ctx) - log.Error().Msg("home support enabled but no user in context") - err = errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx") - case t.lu.Options.Owner != "": - // fallback to owner? - err = createNode(node, &userpb.UserId{ - OpaqueId: t.lu.Options.Owner, - }) - default: - // fallback to parent owner? - err = createNode(node, nil) + // who will become the owner? the owner of the parent node, not the current user + var p *Node + p, err = node.Parent() + if err != nil { + return + } + var owner *userpb.UserId + owner, err = p.Owner() + if err != nil { + return } + err = createNode(node, owner) if err != nil { return nil } @@ -241,15 +232,15 @@ func (t *Tree) Delete(ctx context.Context, n *Node) (err error) { // Prepare the trash // TODO use layout?, but it requires resolving the owners user if the username is used instead of the id. // the node knows the owner id so we use that for now - ownerid, _, err := n.Owner() + o, err := n.Owner() if err != nil { return } - if ownerid == "" { + if o.OpaqueId == "" { // fall back to root trash - ownerid = "root" + o.OpaqueId = "root" } - err = os.MkdirAll(filepath.Join(t.lu.Options.Root, "trash", ownerid), 0700) + err = os.MkdirAll(filepath.Join(t.lu.Options.Root, "trash", o.OpaqueId), 0700) if err != nil { return } @@ -270,7 +261,7 @@ func (t *Tree) Delete(ctx context.Context, n *Node) (err error) { // first make node appear in the owners (or root) trash // parent id and name are stored as extended attributes in the node itself - trashLink := filepath.Join(t.lu.Options.Root, "trash", ownerid, n.ID) + trashLink := filepath.Join(t.lu.Options.Root, "trash", o.OpaqueId, n.ID) err = os.Symlink("../nodes/"+n.ID+".T."+deletionTime, trashLink) if err != nil { // To roll back changes diff --git a/pkg/storage/fs/ocis/upload.go b/pkg/storage/fs/ocis/upload.go index e4d4072410..f531275cdf 100644 --- a/pkg/storage/fs/ocis/upload.go +++ b/pkg/storage/fs/ocis/upload.go @@ -182,6 +182,12 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus log.Debug().Interface("info", info).Interface("node", n).Msg("ocisfs: resolved filename") + // the parent owner will become the new owner + p, perr := n.Parent() + if perr != nil { + return nil, errors.Wrap(perr, "ocisfs: error getting parent "+n.ParentID) + } + // check permissions var ok bool if n.Exists { @@ -191,11 +197,6 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus }) } else { // check permissions of parent - p, perr := n.Parent() - if perr != nil { - return nil, errors.Wrap(perr, "ocisfs: error getting parent "+n.ParentID) - } - ok, err = fs.p.HasPermission(ctx, p, func(rp *provider.ResourcePermissions) bool { return rp.InitiateFileUpload }) @@ -214,6 +215,12 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus return nil, errors.Wrap(err, "ocisfs: error resolving upload path") } usr := user.ContextMustGetUser(ctx) + + owner, err := p.Owner() + if err != nil { + return nil, errors.Wrap(err, "ocisfs: error determining owner") + } + info.Storage = map[string]string{ "Type": "OCISStore", "BinPath": binPath, @@ -226,6 +233,9 @@ func (fs *ocisfs) NewUpload(ctx context.Context, info tusd.FileInfo) (upload tus "UserId": usr.Id.OpaqueId, "UserName": usr.Username, + "OwnerIdp": owner.Idp, + "OwnerId": owner.OpaqueId, + "LogLevel": log.GetLevel().String(), } // Create binary file in the upload folder with no content @@ -417,25 +427,14 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { Msg("ocisfs: could not rename") return } - // who will become the owner? - u, ok := user.ContextGetUser(upload.ctx) - switch { - case ok: - err = n.writeMetadata(u.Id) - case upload.fs.o.EnableHome: - log := appctx.GetLogger(upload.ctx) - log.Error().Msg("home support enabled but no user in context") - err = errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from upload ctx") - case upload.fs.o.Owner != "": - err = n.writeMetadata(&userpb.UserId{ - OpaqueId: upload.fs.o.Owner, - }) - default: - // fallback to parent owner? - err = n.writeMetadata(nil) - } + + // who will become the owner? the owner of the parent actually ... not the currently logged in user + err = n.writeMetadata(&userpb.UserId{ + Idp: upload.info.Storage["OwnerIdp"], + OpaqueId: upload.info.Storage["OwnerId"], + }) if err != nil { - return + return errors.Wrap(err, "ocisfs: could not write metadata") } // link child name to parent if it is new diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 22fb636eb5..15f0aff408 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -523,6 +523,69 @@ func (fs *ocfs) getUser(ctx context.Context, usernameOrID string) (id *userpb.Us return res.User, nil } +// permissionSet returns the permission set for the current user +func (fs *ocfs) permissionSet(ctx context.Context, owner *userpb.UserId) *provider.ResourcePermissions { + if owner == nil { + return &provider.ResourcePermissions{ + Stat: true, + } + } + u, ok := user.ContextGetUser(ctx) + if !ok { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id == nil { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id.OpaqueId == owner.OpaqueId && u.Id.Idp == owner.Idp { + return &provider.ResourcePermissions{ + // owner has all permissions + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } + } + // TODO fix permissions for share recipients by traversing reading acls up to the root? cache acls for the parent node and reuse it + return &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } +} func (fs *ocfs) convertToResourceInfo(ctx context.Context, fi os.FileInfo, ip string, sp string, c redis.Conn, mdKeys []string) *provider.ResourceInfo { id := readOrCreateID(ctx, ip, c) @@ -596,13 +659,12 @@ func (fs *ocfs) convertToResourceInfo(ctx context.Context, fi os.FileInfo, ip st } ri := &provider.ResourceInfo{ - Id: &provider.ResourceId{OpaqueId: id}, - Path: sp, - Type: getResourceType(fi.IsDir()), - Etag: etag, - MimeType: mime.Detect(fi.IsDir(), ip), - Size: uint64(fi.Size()), - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + Id: &provider.ResourceId{OpaqueId: id}, + Path: sp, + Type: getResourceType(fi.IsDir()), + Etag: etag, + MimeType: mime.Detect(fi.IsDir(), ip), + Size: uint64(fi.Size()), Mtime: &types.Timestamp{ Seconds: uint64(fi.ModTime().Unix()), // TODO read nanos from where? Nanos: fi.MTimeNanos, @@ -618,6 +680,8 @@ func (fs *ocfs) convertToResourceInfo(ctx context.Context, fi os.FileInfo, ip st appctx.GetLogger(ctx).Error().Err(err).Msg("error getting owner") } + ri.PermissionSet = fs.permissionSet(ctx, ri.Owner) + return ri } func getResourceType(isDir bool) provider.ResourceType { diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index a943fdb948..078a9efea2 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -142,6 +142,32 @@ type s3FS struct { config *config } +// permissionSet returns the permission set for the current user +func (fs *s3FS) permissionSet(ctx context.Context) *provider.ResourcePermissions { + // TODO fix permissions for share recipients by traversing reading acls up to the root? cache acls for the parent node and reuse it + return &provider.ResourcePermissions{ + // owner has all permissions + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } +} + func (fs *s3FS) normalizeObject(ctx context.Context, o *s3.Object, fn string) *provider.ResourceInfo { fn = fs.removeRoot(path.Join("/", fn)) isDir := strings.HasSuffix(*o.Key, "/") @@ -151,7 +177,7 @@ func (fs *s3FS) normalizeObject(ctx context.Context, o *s3.Object, fn string) *p Type: getResourceType(isDir), Etag: *o.ETag, MimeType: mime.Detect(isDir, fn), - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + PermissionSet: fs.permissionSet(ctx), Size: uint64(*o.Size), Mtime: &types.Timestamp{ Seconds: uint64(o.LastModified.Unix()), @@ -180,7 +206,7 @@ func (fs *s3FS) normalizeHead(ctx context.Context, o *s3.HeadObjectOutput, fn st Type: getResourceType(isDir), Etag: *o.ETag, MimeType: mime.Detect(isDir, fn), - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + PermissionSet: fs.permissionSet(ctx), Size: uint64(*o.ContentLength), Mtime: &types.Timestamp{ Seconds: uint64(o.LastModified.Unix()), @@ -200,7 +226,7 @@ func (fs *s3FS) normalizeCommonPrefix(ctx context.Context, p *s3.CommonPrefix) * Type: getResourceType(true), Etag: "TODO(labkode)", MimeType: mime.Detect(true, fn), - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + PermissionSet: fs.permissionSet(ctx), Size: 0, Mtime: &types.Timestamp{ Seconds: 0, @@ -492,13 +518,13 @@ func (fs *s3FS) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []str return nil, errors.Wrap(err, "s3FS: error listing "+fn) } - for _, o := range output.CommonPrefixes { + for i := range output.CommonPrefixes { log.Debug(). - Interface("object", *o). + Interface("object", output.CommonPrefixes[i]). Str("fn", fn). Msg("found CommonPrefix") - if *o.Prefix == fn+"/" { - return fs.normalizeCommonPrefix(ctx, o), nil + if *output.CommonPrefixes[i].Prefix == fn+"/" { + return fs.normalizeCommonPrefix(ctx, output.CommonPrefixes[i]), nil } } @@ -532,12 +558,12 @@ func (fs *s3FS) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys return nil, errors.Wrap(err, "s3FS: error listing "+fn) } - for _, p := range output.CommonPrefixes { - finfos = append(finfos, fs.normalizeCommonPrefix(ctx, p)) + for i := range output.CommonPrefixes { + finfos = append(finfos, fs.normalizeCommonPrefix(ctx, output.CommonPrefixes[i])) } - for _, o := range output.Contents { - finfos = append(finfos, fs.normalizeObject(ctx, o, *o.Key)) + for i := range output.Contents { + finfos = append(finfos, fs.normalizeObject(ctx, output.Contents[i], *output.Contents[i].Key)) } input.ContinuationToken = output.NextContinuationToken diff --git a/pkg/storage/utils/ace/ace.go b/pkg/storage/utils/ace/ace.go index 3a900fb300..faf614082e 100644 --- a/pkg/storage/utils/ace/ace.go +++ b/pkg/storage/utils/ace/ace.go @@ -203,6 +203,7 @@ func (e *ACE) grantPermissionSet() *provider.ResourcePermissions { // r if strings.Contains(e.permissions, "r") { p.Stat = true + p.GetPath = true p.InitiateFileDownload = true p.ListContainer = true } @@ -259,7 +260,6 @@ func (e *ACE) grantPermissionSet() *provider.ResourcePermissions { } // ? - // TODO GetPath if strings.Contains(e.permissions, "q") { p.GetQuota = true } diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 7f1b02482e..f83ea47b0a 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1310,6 +1310,65 @@ func (fs *eosfs) convertToFileReference(ctx context.Context, eosFileInfo *eoscli return info, nil } +// permissionSet returns the permission set for the current user +func (fs *eosfs) permissionSet(ctx context.Context, owner *userpb.UserId) *provider.ResourcePermissions { + u, ok := user.ContextGetUser(ctx) + if !ok { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id == nil { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id.OpaqueId == owner.OpaqueId && u.Id.Idp == owner.Idp { + return &provider.ResourcePermissions{ + // owner has all permissions + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } + } + // TODO fix permissions for share recipients by traversing reading acls up to the root? cache acls for the parent node and reuse it + return &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } +} + func (fs *eosfs) convert(ctx context.Context, eosFileInfo *eosclient.FileInfo) (*provider.ResourceInfo, error) { path, err := fs.unwrap(ctx, eosFileInfo.File) if err != nil { @@ -1321,28 +1380,28 @@ func (fs *eosfs) convert(ctx context.Context, eosFileInfo *eosclient.FileInfo) ( size = eosFileInfo.TreeSize } - username, err := fs.getUserIDGateway(ctx, strconv.FormatUint(eosFileInfo.UID, 10)) + owner, err := fs.getUserIDGateway(ctx, strconv.FormatUint(eosFileInfo.UID, 10)) if err != nil { log := appctx.GetLogger(ctx) log.Warn().Uint64("uid", eosFileInfo.UID).Msg("could not lookup userid, leaving empty") - username = &userpb.UserId{} + owner = &userpb.UserId{} } info := &provider.ResourceInfo{ Id: &provider.ResourceId{OpaqueId: fmt.Sprintf("%d", eosFileInfo.Inode)}, Path: path, - Owner: username, + Owner: owner, Etag: fmt.Sprintf("\"%s\"", strings.Trim(eosFileInfo.ETag, "\"")), MimeType: mime.Detect(eosFileInfo.IsDir, path), Size: size, - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + PermissionSet: fs.permissionSet(ctx, owner), Mtime: &types.Timestamp{ Seconds: eosFileInfo.MTimeSec, Nanos: eosFileInfo.MTimeNanos, }, Opaque: &types.Opaque{ Map: map[string]*types.OpaqueEntry{ - "eos": &types.OpaqueEntry{ + "eos": { Decoder: "json", Value: fs.getEosMetadata(eosFileInfo), }, diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index 909b1acd9d..ee7d5ffd4e 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -252,6 +252,65 @@ func (fs *localfs) isShareFolderChild(ctx context.Context, p string) bool { return len(vals) > 1 && vals[1] != "" } +// permissionSet returns the permission set for the current user +func (fs *localfs) permissionSet(ctx context.Context, owner *userpb.UserId) *provider.ResourcePermissions { + u, ok := user.ContextGetUser(ctx) + if !ok { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id == nil { + return &provider.ResourcePermissions{ + // no permissions + } + } + if u.Id.OpaqueId == owner.OpaqueId && u.Id.Idp == owner.Idp { + return &provider.ResourcePermissions{ + // owner has all permissions + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } + } + // TODO fix permissions for share recipients by traversing reading acls up to the root? cache acls for the parent node and reuse it + return &provider.ResourcePermissions{ + AddGrant: true, + CreateContainer: true, + Delete: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + InitiateFileUpload: true, + ListContainer: true, + ListFileVersions: true, + ListGrants: true, + ListRecycle: true, + Move: true, + PurgeRecycle: true, + RemoveGrant: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + Stat: true, + UpdateGrant: true, + } +} + func (fs *localfs) normalize(ctx context.Context, fi os.FileInfo, fn string, mdKeys []string) (*provider.ResourceInfo, error) { fp := fs.unwrap(ctx, path.Join("/", fn)) owner, err := getUser(ctx) @@ -279,7 +338,7 @@ func (fs *localfs) normalize(ctx context.Context, fi os.FileInfo, fn string, mdK Etag: calcEtag(ctx, fi), MimeType: mime.Detect(fi.IsDir(), fp), Size: uint64(fi.Size()), - PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true}, + PermissionSet: fs.permissionSet(ctx, owner.Id), Mtime: &types.Timestamp{ Seconds: uint64(fi.ModTime().Unix()), }, diff --git a/tests/acceptance/expected-failures-on-EOS-storage.txt b/tests/acceptance/expected-failures-on-EOS-storage.txt index 529c698a76..ae9b71c9e4 100644 --- a/tests/acceptance/expected-failures-on-EOS-storage.txt +++ b/tests/acceptance/expected-failures-on-EOS-storage.txt @@ -781,14 +781,6 @@ apiWebdavProperties2/getFileProperties.feature:402 apiWebdavProperties2/getFileProperties.feature:403 # # https://github.com/owncloud/ocis-reva/issues/217 Some failing tests with Webdav custom properties -apiWebdavProperties2/getFileProperties.feature:415 -apiWebdavProperties2/getFileProperties.feature:416 -# -# https://github.com/owncloud/ocis-reva/issues/217 Some failing tests with Webdav custom properties -apiWebdavProperties2/getFileProperties.feature:428 -apiWebdavProperties2/getFileProperties.feature:429 -# -# https://github.com/owncloud/ocis-reva/issues/217 Some failing tests with Webdav custom properties apiWebdavProperties2/getFileProperties.feature:441 apiWebdavProperties2/getFileProperties.feature:442 # diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.txt b/tests/acceptance/expected-failures-on-OCIS-storage.txt index 3d5cd23ebc..ff1fc7577e 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.txt +++ b/tests/acceptance/expected-failures-on-OCIS-storage.txt @@ -369,13 +369,6 @@ apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:61 apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:79 apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:80 # -# https://github.com/owncloud/ocis-reva/issues/47 cannot get ocs:share-permissions via WebDAV -# -apiShareOperationsToShares/getWebDAVSharePermissions.feature:23 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:24 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:142 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:143 -# # https://github.com/owncloud/product/issues/203 file_target in share response # apiShareOperationsToShares/gettingShares.feature:167 @@ -421,9 +414,7 @@ apiSharePublicLink1/changingPublicLinkShare.feature:244 # https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions # apiSharePublicLink1/changingPublicLinkShare.feature:267 -apiSharePublicLink1/changingPublicLinkShare.feature:278 apiSharePublicLink1/changingPublicLinkShare.feature:289 -apiSharePublicLink1/changingPublicLinkShare.feature:300 apiSharePublicLink1/createPublicLinkShare.feature:34 apiSharePublicLink1/createPublicLinkShare.feature:35 apiSharePublicLink1/createPublicLinkShare.feature:183 @@ -437,12 +428,8 @@ apiSharePublicLink1/createPublicLinkShare.feature:371 # # https://github.com/owncloud/ocis-reva/issues/12 Range Header is not obeyed when downloading a file # -apiSharePublicLink1/createPublicLinkShare.feature:63 -apiSharePublicLink1/createPublicLinkShare.feature:64 apiSharePublicLink1/createPublicLinkShare.feature:95 apiSharePublicLink1/createPublicLinkShare.feature:96 -apiSharePublicLink1/createPublicLinkShare.feature:245 -apiSharePublicLink1/createPublicLinkShare.feature:246 apiSharePublicLink1/createPublicLinkShare.feature:276 apiSharePublicLink1/createPublicLinkShare.feature:277 # @@ -455,8 +442,6 @@ apiSharePublicLink1/createPublicLinkShare.feature:156 # # https://github.com/owncloud/ocis-reva/issues/41 various sharing settings cannot be set # -apiSharePublicLink1/createPublicLinkShare.feature:389 -apiSharePublicLink1/createPublicLinkShare.feature:390 apiSharePublicLink1/createPublicLinkShare.feature:410 apiSharePublicLink1/createPublicLinkShare.feature:411 apiSharePublicLink1/createPublicLinkShare.feature:431 @@ -520,24 +505,11 @@ apiSharePublicLink2/updatePublicLinkShare.feature:285 # # https://github.com/owncloud/product/issues/270 [OCIS] share permissions not enforced # -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:25 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:26 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:62 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:63 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:77 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:78 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:136 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:137 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:157 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:158 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:179 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:180 # -# https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions -# -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:97 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:98 -# # https://github.com/owncloud/product/issues/272 [OCIS] old public webdav api doesnt works # apiSharePublicLink2/reShareAsPublicLinkToSharesOldDav.feature:30 @@ -572,12 +544,9 @@ apiSharePublicLink2/updatePublicLinkShare.feature:440 # # https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions # -apiSharePublicLink2/updatePublicLinkShare.feature:461 -apiSharePublicLink2/updatePublicLinkShare.feature:462 apiSharePublicLink2/updatePublicLinkShare.feature:486 apiSharePublicLink2/updatePublicLinkShare.feature:487 apiSharePublicLink2/uploadToPublicLinkShare.feature:9 -apiSharePublicLink2/uploadToPublicLinkShare.feature:74 apiSharePublicLink2/uploadToPublicLinkShare.feature:83 apiSharePublicLink2/uploadToPublicLinkShare.feature:103 apiSharePublicLink2/uploadToPublicLinkShare.feature:121 @@ -595,8 +564,6 @@ apiSharePublicLink2/uploadToPublicLinkShare.feature:273 # # https://github.com/owncloud/ocis-reva/issues/290 Accessing non-existing public link should return 404, not 500 # -apiSharePublicLink2/uploadToPublicLinkShare.feature:62 -apiSharePublicLink2/uploadToPublicLinkShare.feature:63 apiSharePublicLink2/uploadToPublicLinkShare.feature:66 # # https://github.com/owncloud/ocis-reva/issues/195 Set quota over settings @@ -617,10 +584,6 @@ apiSharePublicLink2/uploadToPublicLinkShare.feature:255 # # https://github.com/owncloud/product/issues/265 Resharing does not work with ocis storage # -apiShareReshareToShares1/reShare.feature:24 -apiShareReshareToShares1/reShare.feature:25 -apiShareReshareToShares1/reShare.feature:39 -apiShareReshareToShares1/reShare.feature:40 apiShareReshareToShares1/reShare.feature:56 apiShareReshareToShares1/reShare.feature:57 apiShareReshareToShares1/reShare.feature:72 @@ -629,18 +592,6 @@ apiShareReshareToShares1/reShare.feature:88 apiShareReshareToShares1/reShare.feature:89 apiShareReshareToShares1/reShare.feature:104 apiShareReshareToShares1/reShare.feature:105 -apiShareReshareToShares1/reShare.feature:120 -apiShareReshareToShares1/reShare.feature:121 -apiShareReshareToShares1/reShare.feature:122 -apiShareReshareToShares1/reShare.feature:123 -apiShareReshareToShares1/reShare.feature:124 -apiShareReshareToShares1/reShare.feature:125 -apiShareReshareToShares1/reShare.feature:127 -apiShareReshareToShares1/reShare.feature:128 -apiShareReshareToShares1/reShare.feature:129 -apiShareReshareToShares1/reShare.feature:130 -apiShareReshareToShares1/reShare.feature:131 -apiShareReshareToShares1/reShare.feature:132 apiShareReshareToShares1/reShare.feature:159 apiShareReshareToShares1/reShare.feature:160 apiShareReshareToShares1/reShare.feature:161 @@ -657,42 +608,6 @@ apiShareReshareToShares1/reShare.feature:171 apiShareReshareToShares1/reShare.feature:172 apiShareReshareToShares1/reShare.feature:173 apiShareReshareToShares1/reShare.feature:174 -apiShareReshareToShares1/reShare.feature:189 -apiShareReshareToShares1/reShare.feature:190 -apiShareReshareToShares1/reShare.feature:191 -apiShareReshareToShares1/reShare.feature:192 -apiShareReshareToShares1/reShare.feature:193 -apiShareReshareToShares1/reShare.feature:194 -apiShareReshareToShares1/reShare.feature:195 -apiShareReshareToShares1/reShare.feature:196 -apiShareReshareToShares1/reShare.feature:197 -apiShareReshareToShares1/reShare.feature:198 -apiShareReshareToShares1/reShare.feature:199 -apiShareReshareToShares1/reShare.feature:200 -apiShareReshareToShares1/reShare.feature:202 -apiShareReshareToShares1/reShare.feature:203 -apiShareReshareToShares1/reShare.feature:204 -apiShareReshareToShares1/reShare.feature:205 -apiShareReshareToShares1/reShare.feature:206 -apiShareReshareToShares1/reShare.feature:207 -apiShareReshareToShares1/reShare.feature:208 -apiShareReshareToShares1/reShare.feature:209 -apiShareReshareToShares1/reShare.feature:210 -apiShareReshareToShares1/reShare.feature:211 -apiShareReshareToShares1/reShare.feature:212 -apiShareReshareToShares1/reShare.feature:213 -apiShareReshareToShares1/reShare.feature:228 -apiShareReshareToShares1/reShare.feature:229 -apiShareReshareToShares1/reShare.feature:230 -apiShareReshareToShares1/reShare.feature:231 -apiShareReshareToShares1/reShare.feature:232 -apiShareReshareToShares1/reShare.feature:233 -apiShareReshareToShares1/reShare.feature:235 -apiShareReshareToShares1/reShare.feature:236 -apiShareReshareToShares1/reShare.feature:237 -apiShareReshareToShares1/reShare.feature:238 -apiShareReshareToShares1/reShare.feature:239 -apiShareReshareToShares1/reShare.feature:240 apiShareReshareToShares3/reShareWithExpiryDate.feature:365 apiShareReshareToShares3/reShareWithExpiryDate.feature:366 apiShareReshareToShares3/reShareWithExpiryDate.feature:367 @@ -741,42 +656,6 @@ apiShareReshareToShares2/reShareDisabled.feature:40 # # https://github.com/owncloud/product/issues/270 share permissions are not enforced # -apiShareReshareToShares2/reShareSubfolder.feature:47 -apiShareReshareToShares2/reShareSubfolder.feature:48 -apiShareReshareToShares2/reShareSubfolder.feature:49 -apiShareReshareToShares2/reShareSubfolder.feature:50 -apiShareReshareToShares2/reShareSubfolder.feature:51 -apiShareReshareToShares2/reShareSubfolder.feature:52 -apiShareReshareToShares2/reShareSubfolder.feature:53 -apiShareReshareToShares2/reShareSubfolder.feature:54 -apiShareReshareToShares2/reShareSubfolder.feature:55 -apiShareReshareToShares2/reShareSubfolder.feature:56 -apiShareReshareToShares2/reShareSubfolder.feature:57 -apiShareReshareToShares2/reShareSubfolder.feature:58 -apiShareReshareToShares2/reShareSubfolder.feature:60 -apiShareReshareToShares2/reShareSubfolder.feature:61 -apiShareReshareToShares2/reShareSubfolder.feature:62 -apiShareReshareToShares2/reShareSubfolder.feature:63 -apiShareReshareToShares2/reShareSubfolder.feature:64 -apiShareReshareToShares2/reShareSubfolder.feature:65 -apiShareReshareToShares2/reShareSubfolder.feature:66 -apiShareReshareToShares2/reShareSubfolder.feature:67 -apiShareReshareToShares2/reShareSubfolder.feature:68 -apiShareReshareToShares2/reShareSubfolder.feature:69 -apiShareReshareToShares2/reShareSubfolder.feature:70 -apiShareReshareToShares2/reShareSubfolder.feature:71 -apiShareReshareToShares2/reShareSubfolder.feature:73 -apiShareReshareToShares2/reShareSubfolder.feature:74 -apiShareReshareToShares2/reShareSubfolder.feature:75 -apiShareReshareToShares2/reShareSubfolder.feature:76 -apiShareReshareToShares2/reShareSubfolder.feature:77 -apiShareReshareToShares2/reShareSubfolder.feature:78 -apiShareReshareToShares2/reShareSubfolder.feature:80 -apiShareReshareToShares2/reShareSubfolder.feature:81 -apiShareReshareToShares2/reShareSubfolder.feature:82 -apiShareReshareToShares2/reShareSubfolder.feature:83 -apiShareReshareToShares2/reShareSubfolder.feature:84 -apiShareReshareToShares2/reShareSubfolder.feature:85 apiShareManagementToShares/mergeShare.feature:24 apiShareManagementToShares/mergeShare.feature:52 apiShareManagementToShares/mergeShare.feature:79 @@ -1246,10 +1125,6 @@ apiWebdavProperties2/getFileProperties.feature:327 apiWebdavProperties2/getFileProperties.feature:328 apiWebdavProperties2/getFileProperties.feature:376 apiWebdavProperties2/getFileProperties.feature:377 -apiWebdavProperties2/getFileProperties.feature:415 -apiWebdavProperties2/getFileProperties.feature:416 -apiWebdavProperties2/getFileProperties.feature:428 -apiWebdavProperties2/getFileProperties.feature:429 apiWebdavProperties2/getFileProperties.feature:441 apiWebdavProperties2/getFileProperties.feature:442 apiWebdavProperties2/getFileProperties.feature:454 @@ -1418,8 +1293,6 @@ apiWebdavEtagPropagation1/moveFileFolder.feature:244 apiWebdavEtagPropagation1/moveFileFolder.feature:245 apiWebdavEtagPropagation1/moveFileFolder.feature:314 apiWebdavEtagPropagation1/moveFileFolder.feature:315 -apiWebdavEtagPropagation2/copyFileFolder.feature:158 -apiWebdavEtagPropagation2/copyFileFolder.feature:159 # # https://github.com/owncloud/product/issues/209 Implement Trashbin Feature for ocis storage # @@ -1436,11 +1309,6 @@ apiWebdavEtagPropagation2/restoreFromTrash.feature:91 # apiWebdavEtagPropagation2/restoreVersion.feature:10 # -# https://github.com/owncloud/ocis/issues/762 path and other information are not shown if a share does not have "read" permission -# -apiShareOperationsToShares/uploadToShare.feature:64 -apiShareOperationsToShares/uploadToShare.feature:65 -# # https://github.com/owncloud/product/issues/293 sharing with group not available # apiShareOperationsToShares/uploadToShare.feature:39 @@ -1450,11 +1318,6 @@ apiShareOperationsToShares/uploadToShare.feature:92 apiShareOperationsToShares/uploadToShare.feature:139 apiShareOperationsToShares/uploadToShare.feature:140 # -# https://github.com/owncloud/ocis/issues/763 [OCIS-storage] reading a file that a collaborator uploaded is impossible -# -apiShareOperationsToShares/uploadToShare.feature:114 -apiShareOperationsToShares/uploadToShare.feature:115 -# # https://github.com/owncloud/product/issues/247 changing user quota gives ocs status 103 / Cannot set quota # apiShareOperationsToShares/uploadToShare.feature:162 @@ -1490,18 +1353,11 @@ apiShareOperationsToShares/changingFilesShare.feature:60 # [OCIS-storage] overwriting a file as share receiver, does not create a new file version for the sharer https://github.com/owncloud/ocis/issues/766 # apiVersions/fileVersionsSharingToShares.feature:33 -apiVersions/fileVersionsSharingToShares.feature:56 # # restoring an older version of a shared file deletes the share https://github.com/owncloud/ocis/issues/765 # apiVersions/fileVersionsSharingToShares.feature:44 # -# [OCIS-storage] reading a file that a collaborator uploaded is impossible https://github.com/owncloud/ocis/issues/763 -# -apiVersions/fileVersionsSharingToShares.feature:82 -apiVersions/fileVersionsSharingToShares.feature:95 -apiVersions/fileVersionsSharingToShares.feature:108 -# # https://github.com/owncloud/ocis/issues/560 cannot move from Shares folder # apiVersions/fileVersionsSharingToShares.feature:134 @@ -1847,36 +1703,20 @@ apiShareManagementToShares/moveReceivedShare.feature:70 apiShareManagementToShares/moveReceivedShare.feature:71 apiShareManagementToShares/moveReceivedShare.feature:73 apiShareManagementToShares/moveReceivedShare.feature:88 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:38 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:39 apiShareOperationsToShares/getWebDAVSharePermissions.feature:59 apiShareOperationsToShares/getWebDAVSharePermissions.feature:60 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:73 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:74 apiShareOperationsToShares/getWebDAVSharePermissions.feature:94 apiShareOperationsToShares/getWebDAVSharePermissions.feature:95 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:108 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:109 apiShareOperationsToShares/getWebDAVSharePermissions.feature:129 apiShareOperationsToShares/getWebDAVSharePermissions.feature:130 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:157 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:158 apiShareOperationsToShares/getWebDAVSharePermissions.feature:177 apiShareOperationsToShares/getWebDAVSharePermissions.feature:178 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:191 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:192 apiShareOperationsToShares/getWebDAVSharePermissions.feature:212 apiShareOperationsToShares/getWebDAVSharePermissions.feature:213 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:226 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:227 apiShareOperationsToShares/getWebDAVSharePermissions.feature:247 apiShareOperationsToShares/getWebDAVSharePermissions.feature:248 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:261 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:262 apiShareOperationsToShares/getWebDAVSharePermissions.feature:282 apiShareOperationsToShares/getWebDAVSharePermissions.feature:283 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:296 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:297 apiShareOperationsToShares/getWebDAVSharePermissions.feature:317 apiShareOperationsToShares/getWebDAVSharePermissions.feature:318 apiShareUpdateToShares/updateShare.feature:92 @@ -2277,14 +2117,6 @@ apiWebdavUploadTUS/uploadFileMtimeShares.feature:56 apiWebdavUploadTUS/uploadFileMtimeShares.feature:70 apiWebdavUploadTUS/uploadFileMtimeShares.feature:71 -# https://github.com/owncloud/ocis/issues/968 [ocis-storage] PROPFIND on a file uploaded by share receiver is not possible -apiWebdavUploadTUS/uploadToShare.feature:23 -apiWebdavUploadTUS/uploadToShare.feature:24 -apiWebdavUploadTUS/uploadToShare.feature:37 -apiWebdavUploadTUS/uploadToShare.feature:38 -apiWebdavUploadTUS/uploadToShare.feature:66 -apiWebdavUploadTUS/uploadToShare.feature:67 - # https://github.com/owncloud/product/issues/293 sharing with group not available apiWebdavUploadTUS/uploadToShare.feature:52 apiWebdavUploadTUS/uploadToShare.feature:53 @@ -2307,17 +2139,9 @@ apiWebdavUploadTUS/uploadFile.feature:156 apiShareManagementBasicToShares/createShareToSharesFolder.feature:632 apiShareManagementBasicToShares/createShareToSharesFolder.feature:633 -# https://github.com/owncloud/ocis/issues/1126 share owner cannot delete other user's files -apiShareUpdateToShares/updateShare.feature:384 -apiShareUpdateToShares/updateShare.feature:385 - # https://github.com/owncloud/ocis/issues/541 Deletion time in trash bin shows a wrong date apiTrashbin/trashbinFilesFolders.feature:284 apiTrashbin/trashbinFilesFolders.feature:285 -# https://github.com/owncloud/ocis/issues/763 reading a file that a collaborator uploaded is impossible -apiShareOperationsToShares/uploadToShare.feature:279 -apiShareOperationsToShares/uploadToShare.feature:280 - # https://github.com/owncloud/ocis/issues/766 [OCIS-storage] overwriting a file as share receiver, does not create a new file version for the sharer apiVersions/fileVersionsSharingToShares.feature:291 diff --git a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt index c970d2e5e6..3ec928c247 100644 --- a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt +++ b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt @@ -355,13 +355,6 @@ apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:61 apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:79 apiShareOperationsToShares/gettingSharesSharedFilteredEmpty.feature:80 # -# https://github.com/owncloud/ocis-reva/issues/47 cannot get ocs:share-permissions via WebDAV -# -apiShareOperationsToShares/getWebDAVSharePermissions.feature:23 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:24 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:142 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:143 -# # https://github.com/owncloud/ocis-reva/issues/282 Split old public API webdav tests from new public webdav tests # https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions # https://github.com/owncloud/ocis/issues/187 Previews via webDAV API tests fail on OCIS @@ -393,9 +386,7 @@ apiSharePublicLink1/changingPublicLinkShare.feature:244 # https://github.com/owncloud/ocis/issues/723 upload-only public link does not refer to files-drop page, nor are the permissions enforced # apiSharePublicLink1/changingPublicLinkShare.feature:267 -apiSharePublicLink1/changingPublicLinkShare.feature:278 apiSharePublicLink1/changingPublicLinkShare.feature:289 -apiSharePublicLink1/changingPublicLinkShare.feature:300 apiSharePublicLink1/createPublicLinkShare.feature:34 apiSharePublicLink1/createPublicLinkShare.feature:35 apiSharePublicLink1/createPublicLinkShare.feature:183 @@ -409,12 +400,8 @@ apiSharePublicLink1/createPublicLinkShare.feature:371 # # https://github.com/owncloud/ocis-reva/issues/12 Range Header is not obeyed when downloading a file # -apiSharePublicLink1/createPublicLinkShare.feature:63 -apiSharePublicLink1/createPublicLinkShare.feature:64 apiSharePublicLink1/createPublicLinkShare.feature:95 apiSharePublicLink1/createPublicLinkShare.feature:96 -apiSharePublicLink1/createPublicLinkShare.feature:245 -apiSharePublicLink1/createPublicLinkShare.feature:246 apiSharePublicLink1/createPublicLinkShare.feature:276 apiSharePublicLink1/createPublicLinkShare.feature:277 # @@ -427,8 +414,6 @@ apiSharePublicLink1/createPublicLinkShare.feature:156 # # https://github.com/owncloud/ocis-reva/issues/41 various sharing settings cannot be set # -apiSharePublicLink1/createPublicLinkShare.feature:389 -apiSharePublicLink1/createPublicLinkShare.feature:390 apiSharePublicLink1/createPublicLinkShare.feature:410 apiSharePublicLink1/createPublicLinkShare.feature:411 apiSharePublicLink1/createPublicLinkShare.feature:431 @@ -505,11 +490,6 @@ apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:158 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:179 apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:180 # -# https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions -# -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:97 -apiSharePublicLink2/reShareAsPublicLinkToSharesNewDav.feature:98 -# # https://github.com/owncloud/product/issues/272 [OCIS] old public webdav api doesnt works # apiSharePublicLink2/reShareAsPublicLinkToSharesOldDav.feature:30 @@ -545,12 +525,9 @@ apiSharePublicLink2/updatePublicLinkShare.feature:440 # # https://github.com/owncloud/ocis-reva/issues/292 Public link enforce permissions # -apiSharePublicLink2/updatePublicLinkShare.feature:461 -apiSharePublicLink2/updatePublicLinkShare.feature:462 apiSharePublicLink2/updatePublicLinkShare.feature:486 apiSharePublicLink2/updatePublicLinkShare.feature:487 apiSharePublicLink2/uploadToPublicLinkShare.feature:9 -apiSharePublicLink2/uploadToPublicLinkShare.feature:74 apiSharePublicLink2/uploadToPublicLinkShare.feature:83 apiSharePublicLink2/uploadToPublicLinkShare.feature:103 apiSharePublicLink2/uploadToPublicLinkShare.feature:121 @@ -568,8 +545,6 @@ apiSharePublicLink2/uploadToPublicLinkShare.feature:273 # # https://github.com/owncloud/ocis-reva/issues/290 Accessing non-existing public link should return 404, not 500 # -apiSharePublicLink2/uploadToPublicLinkShare.feature:62 -apiSharePublicLink2/uploadToPublicLinkShare.feature:63 apiSharePublicLink2/uploadToPublicLinkShare.feature:66 # # https://github.com/owncloud/ocis-reva/issues/195 Set quota over settings @@ -1219,10 +1194,6 @@ apiWebdavProperties2/getFileProperties.feature:327 apiWebdavProperties2/getFileProperties.feature:328 apiWebdavProperties2/getFileProperties.feature:376 apiWebdavProperties2/getFileProperties.feature:377 -apiWebdavProperties2/getFileProperties.feature:415 -apiWebdavProperties2/getFileProperties.feature:416 -apiWebdavProperties2/getFileProperties.feature:428 -apiWebdavProperties2/getFileProperties.feature:429 apiWebdavProperties2/getFileProperties.feature:441 apiWebdavProperties2/getFileProperties.feature:442 apiWebdavProperties2/getFileProperties.feature:454 @@ -1351,11 +1322,6 @@ apiShareOperationsToShares/gettingShares.feature:168 # apiSharePublicLink2/multilinkSharing.feature:181 # -# https://github.com/owncloud/ocis/issues/762 path and other information are not shown if a share does not have "read" permission -# -apiShareOperationsToShares/uploadToShare.feature:64 -apiShareOperationsToShares/uploadToShare.feature:65 -# # https://github.com/owncloud/product/issues/293 sharing with group not available # apiShareOperationsToShares/uploadToShare.feature:39 @@ -1786,8 +1752,6 @@ apiShareManagementToShares/moveReceivedShare.feature:70 apiShareManagementToShares/moveReceivedShare.feature:71 apiShareManagementToShares/moveReceivedShare.feature:73 apiShareManagementToShares/moveReceivedShare.feature:88 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:38 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:39 apiShareOperationsToShares/getWebDAVSharePermissions.feature:59 apiShareOperationsToShares/getWebDAVSharePermissions.feature:60 apiShareOperationsToShares/getWebDAVSharePermissions.feature:73 @@ -1798,8 +1762,6 @@ apiShareOperationsToShares/getWebDAVSharePermissions.feature:108 apiShareOperationsToShares/getWebDAVSharePermissions.feature:109 apiShareOperationsToShares/getWebDAVSharePermissions.feature:129 apiShareOperationsToShares/getWebDAVSharePermissions.feature:130 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:157 -apiShareOperationsToShares/getWebDAVSharePermissions.feature:158 apiShareOperationsToShares/getWebDAVSharePermissions.feature:177 apiShareOperationsToShares/getWebDAVSharePermissions.feature:178 apiShareOperationsToShares/getWebDAVSharePermissions.feature:191 @@ -2234,13 +2196,6 @@ apiWebdavUploadTUS/uploadFile.feature:156 apiShareManagementBasicToShares/createShareToSharesFolder.feature:632 apiShareManagementBasicToShares/createShareToSharesFolder.feature:633 -# https://github.com/owncloud/ocis/issues/1126 share owner cannot delete other user's files -apiShareUpdateToShares/updateShare.feature:384 -apiShareUpdateToShares/updateShare.feature:385 - # https://github.com/owncloud/ocis/issues/541 Deletion time in trash bin shows a wrong date apiTrashbin/trashbinFilesFolders.feature:284 -apiTrashbin/trashbinFilesFolders.feature:285 - -# https://github.com/owncloud/ocis/issues/763 reading a file that a collaborator uploaded is impossible -apiShareOperationsToShares/uploadToShare.feature:280 +apiTrashbin/trashbinFilesFolders.feature:285 \ No newline at end of file diff --git a/tests/acceptance/features/apiOcisSpecific/apiShareOperations-getWebDAVSharePermissions.feature b/tests/acceptance/features/apiOcisSpecific/apiShareOperations-getWebDAVSharePermissions.feature deleted file mode 100644 index f7881377fa..0000000000 --- a/tests/acceptance/features/apiOcisSpecific/apiShareOperations-getWebDAVSharePermissions.feature +++ /dev/null @@ -1,23 +0,0 @@ -@api @files_sharing-app-required @issue-ocis-reva-47 -Feature: sharing - - Background: - Given using OCS API version "1" - And these users have been created with default attributes and without skeleton files: - | username | - | Alice | - | Brian | - - @issue-ocis-reva-47 - # after fixing all issues delete this Scenario and use the one from oC10 core - Scenario Outline: Empty webdav share-permissions for owned file - Given using DAV path - And user "Alice" has uploaded file with content "foo" to "/tmp.txt" - When user "Alice" gets the following properties of file "/tmp.txt" using the WebDAV API - | propertyName | - | ocs:share-permissions | - Then the single response should contain a property "ocs:share-permissions" with value "5" - Examples: - | dav-path | - | old | - | new | diff --git a/tests/acceptance/features/apiOcisSpecific/apiSharePublicLink2-uploadToPublicLinkShare.feature b/tests/acceptance/features/apiOcisSpecific/apiSharePublicLink2-uploadToPublicLinkShare.feature deleted file mode 100644 index 320ce525b6..0000000000 --- a/tests/acceptance/features/apiOcisSpecific/apiSharePublicLink2-uploadToPublicLinkShare.feature +++ /dev/null @@ -1,17 +0,0 @@ -@api @files_sharing-app-required @public_link_share-feature-required @skipOnOcis-EOS-Storage @issue-ocis-reva-315 @issue-ocis-reva-316 - -Feature: upload to a public link share - - Background: - Given user "Alice" has been created with default attributes and skeleton files - - @issue-ocis-reva-290 - # after fixing all issues delete this Scenario and use the one from oC10 core - Scenario: Uploading file to a public upload-only share that was deleted does not work - Given the administrator has enabled DAV tech_preview - And user "Alice" has created a public link share with settings - | path | FOLDER | - | permissions | create | - When user "Alice" deletes file "/FOLDER" using the WebDAV API - And the public uploads file "does-not-matter.txt" with content "does not matter" using the new public WebDAV API - Then the HTTP status code should be "500" diff --git a/tests/oc-integration-tests/local/frontend.toml b/tests/oc-integration-tests/local/frontend.toml index d0e377b06f..9d74dc17a8 100644 --- a/tests/oc-integration-tests/local/frontend.toml +++ b/tests/oc-integration-tests/local/frontend.toml @@ -47,7 +47,7 @@ chunk_folder = "/var/tmp/reva/chunks" # for eos we need to rewrite the path # TODO strip the username from the path so the CS3 namespace can be mounted # at the files/ endpoint? what about migration? separate reva instance -files_namespace = "/oc" +files_namespace = "/users" # similar to the dav/files endpoint we can configure a prefix for the old webdav endpoint # we use the old webdav endpoint to present the cs3 namespace diff --git a/tests/oc-integration-tests/local/gateway.toml b/tests/oc-integration-tests/local/gateway.toml index f78238b0ed..8adea69f59 100644 --- a/tests/oc-integration-tests/local/gateway.toml +++ b/tests/oc-integration-tests/local/gateway.toml @@ -58,7 +58,7 @@ home_provider = "/home" "/home" = "localhost:12000" # mount a storage provider without a path wrapper for direct access to users. -"/oc" = "localhost:11000" +"/users" = "localhost:11000" "123e4567-e89b-12d3-a456-426655440000" = "localhost:11000" # another mount point might be "/projects/" diff --git a/tests/oc-integration-tests/local/storage-home.toml b/tests/oc-integration-tests/local/storage-home.toml index 44502b2b71..60e5b15ccf 100644 --- a/tests/oc-integration-tests/local/storage-home.toml +++ b/tests/oc-integration-tests/local/storage-home.toml @@ -22,29 +22,32 @@ address = "0.0.0.0:12000" # this is where clients can find it # the context path wrapper reads tho username from the context and prefixes the relative storage path with it [grpc.services.storageprovider] -driver = "owncloud" +driver = "ocis" mount_path = "/home" mount_id = "123e4567-e89b-12d3-a456-426655440000" expose_data_server = true -data_server_url = "http://revad-services:12001/data" +data_server_url = "http://localhost:12001/data" enable_home_creation = true -[grpc.services.storageprovider.drivers.owncloud] -datadirectory = "/var/tmp/reva/data" +[grpc.services.storageprovider.drivers.ocis] +root = "/var/tmp/reva/data" enable_home = true -redis = "localhost:6379" -userprovidersvc = "localhost:18000" +treetime_accounting = true +treesize_accounting = true +#user_layout = +# do we need owner for users? +#owner = 95cb8724-03b2-11eb-a0a6-c33ef8ef53ad [http] address = "0.0.0.0:12001" [http.services.dataprovider] -driver = "owncloud" +driver = "ocis" temp_folder = "/var/tmp/reva/tmp" -[http.services.dataprovider.drivers.owncloud] -datadirectory = "/var/tmp/reva/data" +[http.services.dataprovider.drivers.ocis] +root = "/var/tmp/reva/data" enable_home = true -redis = "localhost:6379" -userprovidersvc = "localhost:18000" +treetime_accounting = true +treesize_accounting = true diff --git a/tests/oc-integration-tests/local/storage-oc.toml b/tests/oc-integration-tests/local/storage-users.toml similarity index 66% rename from tests/oc-integration-tests/local/storage-oc.toml rename to tests/oc-integration-tests/local/storage-users.toml index a7d1e3d84d..c2d4d0abf4 100644 --- a/tests/oc-integration-tests/local/storage-oc.toml +++ b/tests/oc-integration-tests/local/storage-users.toml @@ -14,25 +14,27 @@ address = "0.0.0.0:11000" # This is a storage provider that grants direct access to the wrapped storage # we have a locally running dataprovider [grpc.services.storageprovider] -driver = "owncloud" -mount_path = "/oc" +driver = "ocis" +mount_path = "/users" mount_id = "123e4567-e89b-12d3-a456-426655440000" expose_data_server = true -data_server_url = "http://revad-services:11001/data" +data_server_url = "http://localhost:11001/data" -[grpc.services.storageprovider.drivers.owncloud] -datadirectory = "/var/tmp/reva/data" -redis = "localhost:6379" -userprovidersvc = "localhost:18000" +[grpc.services.storageprovider.drivers.ocis] +root = "/var/tmp/reva/data" +enable_home = false +treetime_accounting = true +treesize_accounting = true [http] address = "0.0.0.0:11001" [http.services.dataprovider] -driver = "owncloud" +driver = "ocis" temp_folder = "/var/tmp/reva/tmp" -[http.services.dataprovider.drivers.owncloud] -datadirectory = "/var/tmp/reva/data" -redis = "localhost:6379" -userprovidersvc = "localhost:18000" +[http.services.dataprovider.drivers.ocis] +root = "/var/tmp/reva/data" +enable_home = false +treetime_accounting = true +treesize_accounting = true