Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Commit

Permalink
Add OCM scope and webdav endpoint (cs3org#3691)
Browse files Browse the repository at this point in the history
* add ocm scope

* add ocmshares authentication

* renamed storage driver for exposing ocm received shares

* implemented storage driver for exposing ocm shares

* register ocm storage providers

* update go-cs3apis

* enable webdav enpoint for ocm

* restrict ocm scope

* set role in ocmshares authentication

* use ocm webdav endpoint for ocm shares

* implemented ocm share get by token in sql driver

* implemented ocm shares get by token in json driver

* add logs to ocmshares auth

* default namespace for ocm webdav

* add logs to ocmshare auth in the webdav layer

* expose get accepted users

* implements get ocm share by token

* suppoprt multiple protocols when downloading

* tests for ocm shares with the webdav endpoint

* use gmgigi fork for go-cs3apis

* fix base path webdav endpoint

* fix stat

* fix stat

* fix on behalf of the owner

* fix config

* fix context passed to auth

* fix owner ctx

* fix2

* run all ops from share creator

* fix download

* run other ops on behalf of share creator

* fix config for tests

* fix resource id

* fix permissions

* fix linter

* fix path traslation for resource ids

* add ocmshares auth creadential strategy

* open /ocs/v1.php/cloud/user to ocmshare scope

* implemented locks in ocm storage driver

* verify in auth layer permissions for locks

* fix tests config

* always do operations on behalf of the share creator

* fix false positive failed tests

* do not change resoure id to enable app collaborations

* check nested resource for ocm shares

* add changelog

* revert integration tests

* update go cs3apis bindings

* fix unit tests

* fix nested resource in scope check

* fix extract ref for ocm scope

* fix permissions on ocm shared resource

* fix ocm user

* fix ocm share role str

* fix linter

* accept path in open in app
  • Loading branch information
gmgigi96 authored Mar 7, 2023
1 parent a83cc99 commit a4a671d
Show file tree
Hide file tree
Showing 33 changed files with 1,523 additions and 78 deletions.
10 changes: 10 additions & 0 deletions changelog/unreleased/ocm-webdav-and-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Enhancement: Add OCM scope and webdav endpoint

Adds the OCM scope and the ocmshares authentication,
to authenticate the federated user to use the OCM shared
resources.
It also adds the (unprotected) webdav endpoint used to interact with
the shared resources.

https://github.com/cs3org/reva/pull/3691
https://github.com/cs3org/reva/issues/2739
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/cheggaaa/pb v1.0.29
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e
github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870
github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49
github.com/dgraph-io/ristretto v0.1.1
github.com/dolthub/go-mysql-server v0.14.0
github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870 h1:MUYOLg0HxBYDmrtZONje+49yanhqGKmYvishv7GaSvw=
github.com/cs3org/go-cs3apis v0.0.0-20230221082129-bcf2b5cf8870/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49 h1:CG65qpsSttrPAqdK19kaZaAiRadZ5xNFVrKoIjICBBM=
github.com/cs3org/go-cs3apis v0.0.0-20230228180528-ee4e51c97a49/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -375,6 +375,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9 h1:3um08ooi0/lyRmK2eE1XTKmRQHDzPu0IvpCPMljyMZ8=
github.com/glpatcern/go-mime v0.0.0-20221026162842-2a8d71ad17a9/go.mod h1:EJaddanP+JfU3UkVvn0rYYF3b/gD7eZRejbTHqiQExA=
github.com/gmgigi96/go-cs3apis v0.0.0-20230228153318-d227be9140af h1:HmFIcBqhz0IM5NxoCN8jYZY5Ms9PQp2QXshTjGzr0us=
github.com/gmgigi96/go-cs3apis v0.0.0-20230228153318-d227be9140af/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/go-acme/lego/v4 v4.4.0/go.mod h1:l3+tFUFZb590dWcqhWZegynUthtaHJbG2fevUpoOOE0=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
Expand Down
32 changes: 30 additions & 2 deletions internal/grpc/interceptors/auth/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
Expand Down Expand Up @@ -73,11 +74,14 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s
if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}

case strings.HasPrefix(k, "share"):
if err = resolveUserShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "ocmshare"):
if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
}
if err != nil {
log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k)
Expand Down Expand Up @@ -223,6 +227,15 @@ func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *aut
return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}

func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share ocmv1beta1.Share
if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil {
return err
}

return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}

func resolveUserShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share collaboration.Share
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
Expand All @@ -248,6 +261,13 @@ func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, r
return errtypes.PermissionDenied("request is not for a nested resource")
}

func isRelativePathOrEmpty(path string) bool {
if len(path) == 0 {
return true
}
return path[0] != '/'
}

func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) (bool, error) {
// Since the resource ID is obtained from the scope, the current token
// has access to it.
Expand All @@ -261,7 +281,7 @@ func checkIfNestedResource(ctx context.Context, ref *provider.Reference, parent
parentPath := statResponse.Info.Path

childPath := ref.GetPath()
if childPath == "" {
if isRelativePathOrEmpty(childPath) {
// We mint a token as the owner of the public share and try to stat the reference
// TODO(ishank011): We need to find a better alternative to this

Expand Down Expand Up @@ -308,6 +328,8 @@ func extractRefForReaderRole(req interface{}) (*provider.Reference, bool) {
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *gateway.OpenInAppRequest:
return v.GetRef(), true
case *provider.GetLockRequest:
return v.GetRef(), true

// App provider requests
case *appregistry.GetAppProvidersRequest:
Expand Down Expand Up @@ -346,6 +368,12 @@ func extractRefForEditorRole(req interface{}) (*provider.Reference, bool) {
return v.GetRef(), true
case *provider.UnsetArbitraryMetadataRequest:
return v.GetRef(), true
case *provider.SetLockRequest:
return v.GetRef(), true
case *provider.RefreshLockRequest:
return v.GetRef(), true
case *provider.UnlockRequest:
return v.GetRef(), true
}

return nil, false
Expand Down
14 changes: 14 additions & 0 deletions internal/grpc/services/gateway/ocmshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc
return res, nil
}

func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
c, err := pool.GetOCMShareProviderClient(pool.Endpoint(s.c.OCMShareProviderEndpoint))
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareProviderClient")
}

res, err := c.GetOCMShareByToken(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShareByToken")
}

return res, nil
}

// TODO(labkode): read GetShare comment.
func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(pool.Endpoint(s.c.OCMShareProviderEndpoint))
Expand Down
32 changes: 30 additions & 2 deletions internal/grpc/services/ocminvitemanager/ocminvitemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/cs3org/reva/pkg/rgrpc/status"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/pkg/sharedconf"
"github.com/cs3org/reva/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"google.golang.org/grpc"
Expand Down Expand Up @@ -132,7 +133,7 @@ func (s *service) Close() error {
}

func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite"}
return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"}
}

func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
Expand Down Expand Up @@ -313,7 +314,12 @@ func isTokenValid(token *invitepb.InviteToken) bool {
}

func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
user, ok := getUserFilter(ctx, req)
if !ok {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInvalidArg(ctx, "user not found"),
}, nil
}
remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId())
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Expand All @@ -327,6 +333,28 @@ func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAccepted
}, nil
}

func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) {
user, ok := ctxpkg.ContextGetUser(ctx)
if ok {
return user, true
}

if req.Opaque == nil || req.Opaque.Map == nil {
return nil, false
}

v, ok := req.Opaque.Map["user-filter"]
if !ok {
return nil, false
}

var u userpb.UserId
if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil {
return nil, false
}
return &userpb.User{Id: &u}, true
}

func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter())
Expand Down
58 changes: 40 additions & 18 deletions internal/grpc/services/ocmshareprovider/ocmshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ type config struct {
ClientTimeout int `mapstructure:"client_timeout"`
ClientInsecure bool `mapstructure:"client_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc"`
WebDAVPrefix string `mapstructure:"webdav_prefix"`
ProviderDomain string `mapstructure:"provider_domain" docs:"The same domain registered in the provider authorizer"`
WebDAVEndpoint string `mapstructure:"webdav_endpoint"`
WebappTemplate string `mapstructure:"webapp_template"`
}

Expand Down Expand Up @@ -151,7 +151,7 @@ func (s *service) Close() error {
}

func (s *service) UnprotectedEndpoints() []string {
return nil
return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"}
}

func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
Expand All @@ -177,17 +177,13 @@ func getResourceType(info *providerpb.ResourceInfo) string {
return "unknown"
}

func (s *service) webdavURL(ctx context.Context, path string) string {
// the url is in the form of https://cernbox.cern.ch/remote.php/dav/files/gdelmont/eos/user/g/gdelmont
user := ctxpkg.ContextMustGetUser(ctx)
p, err := url.JoinPath(s.conf.WebDAVPrefix, user.Username, path)
if err != nil {
panic(err)
}
func (s *service) webdavURL(ctx context.Context, share *ocm.Share) string {
// the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token
p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/remote.php/dav/ocm", share.Token)
return p
}

func (s *service) getWebdavProtocol(ctx context.Context, info *providerpb.ResourceInfo, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV {
func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV {
var perms []string
if m.WebdavOptions.Permissions.InitiateFileDownload {
perms = append(perms, "read")
Expand All @@ -197,9 +193,8 @@ func (s *service) getWebdavProtocol(ctx context.Context, info *providerpb.Resour
}

return &ocmd.WebDAV{
SharedSecret: ctxpkg.ContextMustGetToken(ctx), // TODO: change this and use an ocm token
Permissions: perms,
URL: s.webdavURL(ctx, info.Path), // TODO: change this and use an endpoint for ocm
Permissions: perms,
URL: s.webdavURL(ctx, share),
}
}

Expand All @@ -213,12 +208,12 @@ func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp {
}
}

func (s *service) getProtocols(ctx context.Context, share *ocm.Share, info *providerpb.ResourceInfo, methods []*ocm.AccessMethod) ocmd.Protocols {
func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols {
var p ocmd.Protocols
for _, m := range methods {
for _, m := range share.AccessMethods {
switch t := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
p = append(p, s.getWebdavProtocol(ctx, info, t))
p = append(p, s.getWebdavProtocol(ctx, share, t))
case *ocm.AccessMethod_WebappOptions:
p = append(p, s.getWebappProtocol(share))
case *ocm.AccessMethod_TransferOptions:
Expand Down Expand Up @@ -308,7 +303,7 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq
SenderDisplayName: user.DisplayName,
ShareType: "user",
ResourceType: getResourceType(info),
Protocols: s.getProtocols(ctx, ocmshare, info, req.AccessMethods),
Protocols: s.getProtocols(ctx, ocmshare),
}

if req.Expiration != nil {
Expand Down Expand Up @@ -362,7 +357,11 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq
}

func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
// if the request is by token, the user does not need to be in the ctx
var user *userpb.User
if req.Ref.GetToken() == "" {
user = ctxpkg.ContextMustGetUser(ctx)
}
ocmshare, err := s.repo.GetShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
Expand All @@ -381,6 +380,29 @@ func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest)
}, nil
}

func (s *service) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
ocmshare, err := s.repo.GetShare(ctx, nil, &ocm.ShareReference{
Spec: &ocm.ShareReference_Token{
Token: req.Token,
},
})
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareByTokenResponse{
Status: status.NewInternal(ctx, err, "error getting share"),
}, nil
}

return &ocm.GetOCMShareByTokenResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}, nil
}

func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListShares(ctx, user, req.Filters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
// Load core authentication strategies.
_ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/basic"
_ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/bearer"
_ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/ocmshares"
_ "github.com/cs3org/reva/internal/http/interceptors/auth/credential/strategy/publicshares"
// Add your own here.
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2018-2023 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 ocmshares

import (
"fmt"
"net/http"

"github.com/cs3org/reva/internal/http/interceptors/auth/credential/registry"
"github.com/cs3org/reva/pkg/auth"
)

func init() {
registry.Register("ocmshares", New)
}

const (
headerShareToken = "ocm-token"
)

type strategy struct{}

// New returns a new auth strategy that handles public share verification.
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}

func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
token := r.Header.Get(headerShareToken)
if token == "" {
token = r.URL.Query().Get(headerShareToken)
}
if token == "" {
return nil, fmt.Errorf("no ocm token provided")
}

return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil
}

func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
}
Loading

0 comments on commit a4a671d

Please sign in to comment.