diff --git a/internal/http/interceptors/auth/credential/loader/loader.go b/internal/http/interceptors/auth/credential/loader/loader.go index dd1bbf328d..52761fbc6a 100644 --- a/internal/http/interceptors/auth/credential/loader/loader.go +++ b/internal/http/interceptors/auth/credential/loader/loader.go @@ -22,5 +22,6 @@ import ( // Load core authentication strategies. _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/basic" _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/bearer" + _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/ocmshares" // Add your own here. ) diff --git a/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go new file mode 100644 index 0000000000..4e7b26c98e --- /dev/null +++ b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go @@ -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/v2/internal/http/interceptors/auth/credential/registry" + "github.com/cs3org/reva/v2/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? +} diff --git a/pkg/auth/manager/loader/loader.go b/pkg/auth/manager/loader/loader.go index 9fcba05541..b21bc384e1 100644 --- a/pkg/auth/manager/loader/loader.go +++ b/pkg/auth/manager/loader/loader.go @@ -27,6 +27,7 @@ import ( _ "github.com/cs3org/reva/v2/pkg/auth/manager/ldap" _ "github.com/cs3org/reva/v2/pkg/auth/manager/machine" _ "github.com/cs3org/reva/v2/pkg/auth/manager/nextcloud" + _ "github.com/cs3org/reva/v2/pkg/auth/manager/ocmshares" _ "github.com/cs3org/reva/v2/pkg/auth/manager/oidc" _ "github.com/cs3org/reva/v2/pkg/auth/manager/owncloudsql" _ "github.com/cs3org/reva/v2/pkg/auth/manager/publicshares" diff --git a/pkg/auth/manager/ocmshares/ocmshares.go b/pkg/auth/manager/ocmshares/ocmshares.go new file mode 100644 index 0000000000..5644650376 --- /dev/null +++ b/pkg/auth/manager/ocmshares/ocmshares.go @@ -0,0 +1,187 @@ +// 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 ( + "context" + + provider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocminvite "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/auth" + "github.com/cs3org/reva/v2/pkg/auth/manager/registry" + "github.com/cs3org/reva/v2/pkg/auth/scope" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/cs3org/reva/v2/pkg/utils/cfg" + "github.com/pkg/errors" +) + +func init() { + registry.Register("ocmshares", New) +} + +type manager struct { + c *config + gw gateway.GatewayAPIClient +} + +type config struct { + GatewayAddr string `mapstructure:"gatewaysvc"` +} + +func (c *config) ApplyDefaults() { + c.GatewayAddr = sharedconf.GetGatewaySVC(c.GatewayAddr) +} + +// New creates a new ocmshares authentication manager. +func New(m map[string]interface{}) (auth.Manager, error) { + var mgr manager + if err := mgr.Configure(m); err != nil { + return nil, err + } + gw, err := pool.GetGatewayServiceClient(mgr.c.GatewayAddr) + if err != nil { + return nil, err + } + mgr.gw = gw + + return &mgr, nil +} + +func (m *manager) Configure(ml map[string]interface{}) error { + var c config + if err := cfg.Decode(ml, &c); err != nil { + return errors.Wrap(err, "ocmshares: error decoding config") + } + m.c = &c + return nil +} + +func (m *manager) Authenticate(ctx context.Context, token, _ string) (*userpb.User, map[string]*authpb.Scope, error) { + log := appctx.GetLogger(ctx).With().Str("token", token).Logger() + shareRes, err := m.gw.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{ + Token: token, + }) + + switch { + case err != nil: + log.Error().Err(err).Msg("error getting ocm share by token") + return nil, nil, err + case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + log.Debug().Msg("ocm share not found") + return nil, nil, errtypes.NotFound(shareRes.Status.Message) + case shareRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + log.Debug().Msg("permission denied") + return nil, nil, errtypes.InvalidCredentials(shareRes.Status.Message) + case shareRes.Status.Code != rpc.Code_CODE_OK: + log.Error().Interface("status", shareRes.Status).Msg("got unexpected error in the grpc call to GetOCMShare") + return nil, nil, errtypes.InternalError(shareRes.Status.Message) + } + + // the user authenticated using the ocmshares authentication method + // is the recipient of the share + u := shareRes.Share.Grantee.GetUserId() + + d, err := utils.MarshalProtoV1ToJSON(shareRes.GetShare().Creator) + if err != nil { + return nil, nil, err + } + + o := &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "user-filter": { + Decoder: "json", + Value: d, + }, + }, + } + + userRes, err := m.gw.GetAcceptedUser(ctx, &ocminvite.GetAcceptedUserRequest{ + RemoteUserId: u, + Opaque: o, + }) + + switch { + case err != nil: + return nil, nil, err + case userRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + return nil, nil, errtypes.NotFound(shareRes.Status.Message) + case userRes.Status.Code != rpc.Code_CODE_OK: + return nil, nil, errtypes.InternalError(userRes.Status.Message) + } + + role, roleStr := getRole(shareRes.Share) + + scope, err := scope.AddOCMShareScope(shareRes.Share, role, nil) + if err != nil { + return nil, nil, err + } + + user := userRes.RemoteUser + user.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "ocm-share-role": { + Decoder: "plain", + Value: []byte(roleStr), + }, + }, + } + + return user, scope, nil +} + +func getRole(s *ocm.Share) (authpb.Role, string) { + // TODO: consider to somehow merge the permissions from all the access methods? + // it's not clear infact which should be the role when webdav is editor role while + // webapp is only view mode for example + // this implementation considers only the simple case in which when a client creates + // a share with multiple access methods, the permissions are matching in all of them. + for _, m := range s.AccessMethods { + switch v := m.Term.(type) { + case *ocm.AccessMethod_WebdavOptions: + p := v.WebdavOptions.Permissions + if p.InitiateFileUpload { + return authpb.Role_ROLE_EDITOR, "editor" + } + if p.InitiateFileDownload { + return authpb.Role_ROLE_VIEWER, "viewer" + } + case *ocm.AccessMethod_WebappOptions: + viewMode := v.WebappOptions.ViewMode + if viewMode == provider.ViewMode_VIEW_MODE_VIEW_ONLY || + viewMode == provider.ViewMode_VIEW_MODE_READ_ONLY || + viewMode == provider.ViewMode_VIEW_MODE_PREVIEW { + return authpb.Role_ROLE_VIEWER, "viewer" + } + if viewMode == provider.ViewMode_VIEW_MODE_READ_WRITE { + return authpb.Role_ROLE_EDITOR, "editor" + } + } + } + return authpb.Role_ROLE_INVALID, "invalid" +} diff --git a/pkg/auth/scope/ocmshare.go b/pkg/auth/scope/ocmshare.go new file mode 100644 index 0000000000..e82ab1a4f6 --- /dev/null +++ b/pkg/auth/scope/ocmshare.go @@ -0,0 +1,157 @@ +// 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 scope + +import ( + "context" + "path/filepath" + "strings" + + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/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" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/rs/zerolog" +) + +// FIXME: the namespace here is hardcoded +// find a way to pass it from the config. +const ocmNamespace = "/ocm" + +func ocmShareScope(_ context.Context, scope *authpb.Scope, resource interface{}, _ *zerolog.Logger) (bool, error) { + var share ocmv1beta1.Share + if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil { + return false, err + } + + switch v := resource.(type) { + // viewer role + case *registry.GetStorageProvidersRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.StatRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.ListContainerRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.InitiateFileDownloadRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *appprovider.OpenInAppRequest: + return checkStorageRefForOCMShare(&share, &provider.Reference{ResourceId: v.ResourceInfo.Id}, ocmNamespace), nil + case *gateway.OpenInAppRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.GetLockRequest: + return checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + + // editor role + case *provider.CreateContainerRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.TouchFileRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.DeleteRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.MoveRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetSource(), ocmNamespace) && checkStorageRefForOCMShare(&share, v.GetDestination(), ocmNamespace), nil + case *provider.InitiateFileUploadRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.SetArbitraryMetadataRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.UnsetArbitraryMetadataRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.SetLockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.RefreshLockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + case *provider.UnlockRequest: + return hasRoleEditor(*scope) && checkStorageRefForOCMShare(&share, v.GetRef(), ocmNamespace), nil + + // App provider requests + case *appregistry.GetDefaultAppProviderForMimeTypeRequest: + return true, nil + + case *userv1beta1.GetUserByClaimRequest: + return true, nil + + case *ocmv1beta1.GetOCMShareRequest: + return checkOCMShareRef(&share, v.GetRef()), nil + case string: + return checkResourcePath(v), nil + } + return false, nil +} + +func checkStorageRefForOCMShare(s *ocmv1beta1.Share, r *provider.Reference, ns string) bool { + if r.ResourceId != nil { + return utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) || strings.HasPrefix(r.ResourceId.OpaqueId, s.Token) + } + + // FIXME: the path here is hardcoded + return strings.HasPrefix(r.GetPath(), filepath.Join(ns, s.Token)) +} + +func checkOCMShareRef(s *ocmv1beta1.Share, ref *ocmv1beta1.ShareReference) bool { + return ref.GetToken() == s.Token +} + +// AddOCMShareScope adds the scope to allow access to an OCM share and the share resource. +func AddOCMShareScope(share *ocmv1beta1.Share, role authpb.Role, scopes map[string]*authpb.Scope) (map[string]*authpb.Scope, error) { + // Create a new "scope share" to only expose the required fields `ResourceId` and `Token` to the scope. + scopeShare := ocmv1beta1.Share{ResourceId: share.ResourceId, Token: share.Token} + val, err := utils.MarshalProtoV1ToJSON(&scopeShare) + if err != nil { + return nil, err + } + if scopes == nil { + scopes = make(map[string]*authpb.Scope) + } + + scopes["ocmshare:"+share.Id.OpaqueId] = &authpb.Scope{ + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: val, + }, + Role: role, + } + return scopes, nil +} + +// GetOCMSharesFromScopes returns all OCM shares in the given scope. +func GetOCMSharesFromScopes(scopes map[string]*authpb.Scope) ([]*ocmv1beta1.Share, error) { + var shares []*ocmv1beta1.Share + for k, s := range scopes { + if strings.HasPrefix(k, "ocmshare:") { + res := s.Resource + if res.Decoder != "json" { + return nil, errtypes.InternalError("resource should be json encoded") + } + var share ocmv1beta1.Share + err := utils.UnmarshalJSONToProtoV1(res.Value, &share) + if err != nil { + return nil, err + } + shares = append(shares, &share) + } + } + return shares, nil +} diff --git a/pkg/storage/fs/loader/loader.go b/pkg/storage/fs/loader/loader.go index d79e58eb3c..5053234f16 100644 --- a/pkg/storage/fs/loader/loader.go +++ b/pkg/storage/fs/loader/loader.go @@ -20,6 +20,8 @@ package loader import ( // Load core storage filesystem backends. + _ "github.com/cs3org/reva/v2/pkg/ocm/storage/outcoming" + _ "github.com/cs3org/reva/v2/pkg/ocm/storage/received" _ "github.com/cs3org/reva/v2/pkg/storage/fs/cephfs" _ "github.com/cs3org/reva/v2/pkg/storage/fs/eos" _ "github.com/cs3org/reva/v2/pkg/storage/fs/eosgrpc" diff --git a/tests/integration/grpc/grpc_suite_test.go b/tests/integration/grpc/grpc_suite_test.go index 596db67c50..8bff0f064e 100644 --- a/tests/integration/grpc/grpc_suite_test.go +++ b/tests/integration/grpc/grpc_suite_test.go @@ -123,13 +123,18 @@ func startRevads(configs []RevadConfig, variables map[string]string) (map[string port++ } + tmpBase, err := os.MkdirTemp("", "reva-grpc-integration-tests") + if err != nil { + return nil, errors.Wrapf(err, "Could not create tmpdir") + } for _, c := range configs { ownAddress := addresses[c.Name] ownID := ids[c.Name] filesPath := map[string]string{} // Create a temporary root for this revad - tmpRoot, err := os.MkdirTemp("", "reva-grpc-integration-tests-"+c.Name+"-*-root") + tmpRoot := path.Join(tmpBase, c.Name) + err := os.Mkdir(tmpRoot, 0755) if err != nil { return nil, errors.Wrapf(err, "Could not create tmpdir") } @@ -252,6 +257,7 @@ func startRevads(configs []RevadConfig, variables map[string]string) (map[string fmt.Println("Test failed, keeping root", tmpRoot, "around for debugging") } else { os.RemoveAll(tmpRoot) + os.Remove(tmpBase) // Remove base temp dir if it's empty } return nil },