diff --git a/changelog/unreleased/alias-links.md b/changelog/unreleased/alias-links.md new file mode 100644 index 0000000000..c52258ca42 --- /dev/null +++ b/changelog/unreleased/alias-links.md @@ -0,0 +1,8 @@ +Change: allow link with no or edit permission + +Allow the creation of links with no permissions. These can be used to navigate to a file +that a user has access to. +Allow setting edit permission on single file links (create and delete are still blocked) +Introduce endpoint to get information about a given token + +https://github.com/cs3org/reva/pull/2687 diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 7665ffdabd..9246bf3d7f 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -75,6 +75,9 @@ func (h *DavHandler) init(c *Config) error { return err } h.TrashbinHandler = new(TrashbinHandler) + if err := h.TrashbinHandler.init(c); err != nil { + return err + } h.SpacesHandler = new(SpacesHandler) if err := h.SpacesHandler.init(c); err != nil { @@ -91,7 +94,7 @@ func (h *DavHandler) init(c *Config) error { return err } - return h.TrashbinHandler.init(c) + return nil } func isOwner(userIDorName string, user *userv1beta1.User) bool { diff --git a/internal/http/services/owncloud/ocs/conversions/main.go b/internal/http/services/owncloud/ocs/conversions/main.go index 21be8c9cef..553433417a 100644 --- a/internal/http/services/owncloud/ocs/conversions/main.go +++ b/internal/http/services/owncloud/ocs/conversions/main.go @@ -154,6 +154,24 @@ type ShareeData struct { Remotes []*MatchData `json:"remotes" xml:"remotes>element"` } +// TokenInfo holds token information +type TokenInfo struct { + // for all callers + Token string `json:"token" xml:"token"` + LinkURL string `json:"link_url" xml:"link_url"` + PasswordProtected bool `json:"password_protected" xml:"password_protected"` + + // if not password protected + StorageID string `json:"storage_id" xml:"storage_id"` + OpaqueID string `json:"opaque_id" xml:"opaque_id"` + Path string `json:"path" xml:"path"` + + // if native access + SpacePath string `json:"space_path" xml:"space_path"` + SpaceAlias string `json:"space_alias" xml:"space_alias"` + SpaceURL string `json:"space_url" xml:"space_url"` +} + // ExactMatchesData hold exact matches type ExactMatchesData struct { Users []*MatchData `json:"users" xml:"users>element"` diff --git a/internal/http/services/owncloud/ocs/conversions/permissions.go b/internal/http/services/owncloud/ocs/conversions/permissions.go index 4d860fb694..b3cc3a2eb4 100644 --- a/internal/http/services/owncloud/ocs/conversions/permissions.go +++ b/internal/http/services/owncloud/ocs/conversions/permissions.go @@ -51,7 +51,7 @@ var ( // The value must be in the valid range. func NewPermissions(val int) (Permissions, error) { if val == int(PermissionInvalid) { - return PermissionInvalid, fmt.Errorf("permissions %d out of range %d - %d", val, PermissionRead, PermissionAll) + return PermissionInvalid, nil } else if val < int(PermissionInvalid) || int(PermissionAll) < val { return PermissionInvalid, ErrPermissionNotInRange } diff --git a/internal/http/services/owncloud/ocs/conversions/permissions_test.go b/internal/http/services/owncloud/ocs/conversions/permissions_test.go index ec09a32b7b..8b20338b05 100644 --- a/internal/http/services/owncloud/ocs/conversions/permissions_test.go +++ b/internal/http/services/owncloud/ocs/conversions/permissions_test.go @@ -33,7 +33,6 @@ func TestNewPermissions(t *testing.T) { func TestNewPermissionsWithInvalidValueShouldFail(t *testing.T) { vals := []int{ - int(PermissionInvalid), -1, int(PermissionAll) + 1, } diff --git a/internal/http/services/owncloud/ocs/conversions/role.go b/internal/http/services/owncloud/ocs/conversions/role.go index 6d0e786b29..e7a97b4ddd 100644 --- a/internal/http/services/owncloud/ocs/conversions/role.go +++ b/internal/http/services/owncloud/ocs/conversions/role.go @@ -222,6 +222,15 @@ func NewUploaderRole() *Role { } } +// NewNoneRole creates a role with no permissions +func NewNoneRole() *Role { + return &Role{ + Name: "none", + cS3ResourcePermissions: &provider.ResourcePermissions{}, + ocsPermissions: PermissionInvalid, + } +} + // NewManagerRole creates an manager role func NewManagerRole() *Role { return &Role{ @@ -254,6 +263,10 @@ func NewManagerRole() *Role { // RoleFromOCSPermissions tries to map ocs permissions to a role func RoleFromOCSPermissions(p Permissions) *Role { + if p == PermissionInvalid { + return NewNoneRole() + } + if p.Contain(PermissionRead) { if p.Contain(PermissionWrite) && p.Contain(PermissionCreate) && p.Contain(PermissionDelete) { if p.Contain(PermissionShare) { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees/token.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees/token.go new file mode 100644 index 0000000000..76b2c670c4 --- /dev/null +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees/token.go @@ -0,0 +1,166 @@ +// Copyright 2018-2022 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 sharees + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "path" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + user "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/v2/internal/http/services/owncloud/ocdav/spacelookup" + "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response" + "github.com/cs3org/reva/v2/pkg/appctx" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/utils" + "google.golang.org/grpc/metadata" +) + +// TokenInfo handles http requests regarding tokens +func (h *Handler) TokenInfo(protected bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + log := appctx.GetLogger(r.Context()) + tkn := path.Base(r.URL.Path) + _, pw, _ := r.BasicAuth() + + c, err := pool.GetGatewayServiceClient(h.gatewayAddr) + if err != nil { + // endpoint public - don't exponse information + log.Error().Err(err).Msg("error getting gateway client") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil) + return + } + + t, err := handleGetToken(r.Context(), tkn, pw, c, protected) + if err != nil { + // endpoint public - don't exponse information + log.Error().Err(err).Msg("error while handling GET TokenInfo") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil) + return + } + + response.WriteOCSSuccess(w, r, t) + } +} + +func handleGetToken(ctx context.Context, tkn string, pw string, c gateway.GatewayAPIClient, protected bool) (conversions.TokenInfo, error) { + user, token, passwordProtected, err := getInfoForToken(tkn, pw, c) + if err != nil { + return conversions.TokenInfo{}, err + } + + t, err := buildTokenInfo(user, tkn, token, passwordProtected, c) + if err != nil { + return t, err + } + + if protected && !t.PasswordProtected { + space, status, err := spacelookup.LookUpStorageSpaceByID(ctx, c, t.StorageID) + // add info only if user is able to stat + if err == nil && status.Code == rpc.Code_CODE_OK { + t.SpacePath = utils.ReadPlainFromOpaque(space.Opaque, "path") + t.SpaceAlias = utils.ReadPlainFromOpaque(space.Opaque, "spaceAlias") + t.SpaceURL = path.Join(t.SpaceAlias, t.OpaqueID, t.Path) + } + + } + + return t, nil +} + +func buildTokenInfo(owner *user.User, tkn string, token string, passProtected bool, c gateway.GatewayAPIClient) (conversions.TokenInfo, error) { + t := conversions.TokenInfo{Token: tkn, LinkURL: "/s/" + tkn} + if passProtected { + t.PasswordProtected = true + return t, nil + } + + ctx := ctxpkg.ContextSetToken(context.TODO(), token) + ctx = ctxpkg.ContextSetUser(ctx, owner) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) + + sRes, err := getTokenStatInfo(ctx, c, tkn) + if err != nil || sRes.Status.Code != rpc.Code_CODE_OK { + return t, fmt.Errorf("can't stat resource. %+v %s", sRes, err) + } + + ls := &link.PublicShare{} + _ = json.Unmarshal(sRes.Info.Opaque.Map["link-share"].Value, ls) + + t.StorageID = ls.ResourceId.GetStorageId() + t.OpaqueID = ls.ResourceId.GetOpaqueId() + + return t, nil +} + +func getInfoForToken(tkn string, pw string, c gateway.GatewayAPIClient) (owner *user.User, token string, passwordProtected bool, err error) { + ctx := context.Background() + + res, err := handleBasicAuth(ctx, c, tkn, pw) + if err != nil { + return + } + + switch res.Status.Code { + case rpc.Code_CODE_OK: + // nothing to do + case rpc.Code_CODE_PERMISSION_DENIED: + if res.Status.Message != "wrong password" { + err = errors.New("not found") + return + } + + passwordProtected = true + return + default: + err = fmt.Errorf("authentication returned unsupported status code '%d'", res.Status.Code) + return + } + + return res.User, res.Token, false, nil +} + +func handleBasicAuth(ctx context.Context, c gateway.GatewayAPIClient, token, pw string) (*gateway.AuthenticateResponse, error) { + authenticateRequest := gateway.AuthenticateRequest{ + Type: "publicshares", + ClientId: token, + ClientSecret: "password|" + pw, + } + + return c.Authenticate(ctx, &authenticateRequest) +} + +func getTokenStatInfo(ctx context.Context, client gateway.GatewayAPIClient, token string) (*provider.StatResponse, error) { + return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ + ResourceId: &provider.ResourceId{ + StorageId: utils.PublicStorageProviderID, + OpaqueId: token, + }, + }}) +} 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 c7e8eddbe9..2f4da7830f 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 @@ -497,6 +497,8 @@ func permissionFromRequest(r *http.Request, h *Handler) (*provider.ResourcePermi // Maps oc10 public link permissions to roles var ocPublicPermToRole = map[int]string{ + // Recipients can do nothing + 0: "none", // Recipients can view and download contents. 1: "viewer", // Recipients can view, download and edit single files. 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 58aa838e8b..036bfab017 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 @@ -426,7 +426,7 @@ func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri } permissions := role.OCSPermissions() - if ri != nil && ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + if ri != nil && ri.Type == provider.ResourceType_RESOURCE_TYPE_FILE && permissions != conversions.PermissionInvalid { // Single file shares should never have delete or create permissions permissions &^= conversions.PermissionCreate permissions &^= conversions.PermissionDelete @@ -440,7 +440,7 @@ func (h *Handler) extractPermissions(w http.ResponseWriter, r *http.Request, ri } existingPermissions := conversions.RoleFromResourcePermissions(ri.PermissionSet).OCSPermissions() - if permissions == conversions.PermissionInvalid || !existingPermissions.Contain(permissions) { + if !existingPermissions.Contain(permissions) { return nil, nil, &ocsError{ Code: http.StatusNotFound, Message: "Cannot set the requested share permissions", diff --git a/internal/http/services/owncloud/ocs/ocs.go b/internal/http/services/owncloud/ocs/ocs.go index 03544f54e6..f35659743d 100644 --- a/internal/http/services/owncloud/ocs/ocs.go +++ b/internal/http/services/owncloud/ocs/ocs.go @@ -87,6 +87,8 @@ func (s *svc) Unprotected() []string { return []string{ "/v1.php/config", "/v2.php/config", + "/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected", + "/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected", } } @@ -125,6 +127,10 @@ func (s *svc) routerInit() error { r.Delete("/{shareid}", sharesHandler.RemoveShare) }) r.Get("/sharees", shareesHandler.FindSharees) + r.Route("/tokeninfo", func(r chi.Router) { + r.Get("/protected/{tkn}", shareesHandler.TokenInfo(true)) + r.Get("/unprotected/{tkn}", shareesHandler.TokenInfo(false)) + }) }) // placeholder for notifications diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index 9f646ebc27..9da2e6124e 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -1576,5 +1576,18 @@ _ocs: api compatibility, return correct status code_ - [apiMain/checksums.feature:233](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/checksums.feature#L233) +#### empty permissions on a link is now allowed + +- [apiShareUpdateToShares/updateShare.feature:112](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareUpdateToShares/updateShare.feature#L112) +- [apiShareUpdateToShares/updateShare.feature:113](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareUpdateToShares/updateShare.feature#L113) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:116](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L116) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:117](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L117) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:131](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L131) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:132](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L132) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:26](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L26) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:27](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L27) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:28](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L28) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:29](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L29) + Note: always have an empty line at the end of this file. The bash script that processes this file may not process a scenario reference on the last line. diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index d809ccd151..f3bd70b211 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -1576,5 +1576,18 @@ _ocs: api compatibility, return correct status code_ - [apiMain/checksums.feature:233](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/checksums.feature#L233) +#### empty permissions on a link is now allowed + +- [apiShareUpdateToShares/updateShare.feature:112](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareUpdateToShares/updateShare.feature#L112) +- [apiShareUpdateToShares/updateShare.feature:113](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareUpdateToShares/updateShare.feature#L113) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:116](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L116) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:117](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L117) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:131](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L131) +- [apiShareManagementBasicToShares/createShareToSharesFolder.feature:132](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementBasicToShares/createShareToSharesFolder.feature#L132) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:26](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L26) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:27](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L27) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:28](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L28) +- [apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature:29](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareCreateSpecialToShares2/createShareWithInvalidPermissions.feature#L29) + Note: always have an empty line at the end of this file. The bash script that processes this file may not process a scenario reference on the last line.