Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,94 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appctx
import (
"context"
"runtime"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "appctx"
// NewUnary returns a new unary interceptor that creates the application context.
func NewUnary(log zerolog.Logger, tp trace.TracerProvider) grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc unary")
}
_, file, _, ok := runtime.Caller(1)
if ok {
span.SetAttributes(semconv.CodeFilePathKey.String(file))
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
ctx = appctx.WithTracerProvider(ctx, tp)
res, err := handler(ctx, req)
return res, err
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that creates the application context.
func NewStream(log zerolog.Logger, tp trace.TracerProvider) grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc stream")
}
_, file, _, ok := runtime.Caller(1)
if ok {
span.SetAttributes(semconv.CodeFilePathKey.String(file))
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
wrapped := newWrappedServerStream(ctx, ss)
err := handler(srv, wrapped)
return err
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,320 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package auth
import (
"context"
"sync"
"time"
"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/token"
tokenmgr "github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "auth"
var (
userGroupsCache gcache.Cache
scopeExpansionCache gcache.Cache
cacheOnce sync.Once
)
type config struct {
// TODO(labkode): access a map is more performant as uri as fixed in length
// for SkipMethods.
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
GatewayAddr string `mapstructure:"gateway_addr"`
UserGroupsCacheSize int `mapstructure:"usergroups_cache_size"`
ScopeExpansionCacheSize int `mapstructure:"scope_expansion_cache_size"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "auth: error decoding conf")
return nil, err
}
return c, nil
}
// NewUnary returns a new unary interceptor that adds
// trace information for the request.
func NewUnary(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (grpc.UnaryServerInterceptor, error) {
conf, err := parseConfig(m)
if err != nil {
err = errors.Wrap(err, "auth: error parsing config")
return nil, err
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
conf.GatewayAddr = sharedconf.GetGatewaySVC(conf.GatewayAddr)
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 5000
}
if conf.ScopeExpansionCacheSize == 0 {
conf.ScopeExpansionCacheSize = 5000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
scopeExpansionCache = gcache.New(conf.ScopeExpansionCacheSize).LFU().Build()
})
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, errtypes.NotFound("auth: token manager does not exist: " + conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, errors.Wrap(err, "auth: error creating token manager")
}
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log := appctx.GetLogger(ctx)
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc auth unary")
}
if utils.Skip(info.FullMethod, unprotected) {
log.Debug().Str("method", info.FullMethod).Msg("skipping auth")
// If a token is present, set it anyway, as we might need the user info
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err == nil {
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
}
return handler(ctx, req)
}
tkn, ok := ctxpkg.ContextGetToken(ctx)
if !ok || tkn == "" {
log.Warn().Msg("access token not found or empty")
return nil, status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}
// validate the token and ensure access to the resource is allowed
u, tokenScope, err := dismantleToken(ctx, tkn, req, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return nil, status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
}
if sharedconf.MultiTenantEnabled() && u.GetId().GetType() != userpb.UserType_USER_TYPE_SERVICE && u.GetId().GetTenantId() == "" {
log.Warn().Msg("user has no tenant id, rejecting request")
return nil, status.Errorf(codes.PermissionDenied, "auth: user has no tenant id, rejecting request")
}
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
return handler(ctx, req)
}
return interceptor, nil
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (grpc.StreamServerInterceptor, error) {
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 10000
}
if conf.ScopeExpansionCacheSize == 0 {
conf.ScopeExpansionCacheSize = 10000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
scopeExpansionCache = gcache.New(conf.ScopeExpansionCacheSize).LFU().Build()
})
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, errtypes.NotFound("auth: token manager not found: " + conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, errtypes.NotFound("auth: token manager not found: " + conf.TokenManager)
}
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
log := appctx.GetLogger(ctx)
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "grpc auth new stream")
}
if utils.Skip(info.FullMethod, unprotected) {
log.Debug().Str("method", info.FullMethod).Msg("skipping auth")
// If a token is present, set it anyway, as we might need the user info
// to decide the storage provider.
tkn, ok := ctxpkg.ContextGetToken(ctx)
if ok {
u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err == nil {
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
ss = newWrappedServerStream(ctx, ss)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
}
return handler(srv, ss)
}
tkn, ok := ctxpkg.ContextGetToken(ctx)
if !ok || tkn == "" {
log.Warn().Msg("access token not found")
return status.Errorf(codes.Unauthenticated, "auth: core access token not found")
}
// validate the token and ensure access to the resource is allowed
u, tokenScope, err := dismantleToken(ctx, tkn, ss, tokenManager, conf.GatewayAddr)
if err != nil {
log.Warn().Err(err).Msg("access token is invalid")
return status.Errorf(codes.PermissionDenied, "auth: core access token is invalid")
}
// store user and scopes in context
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
wrapped := newWrappedServerStream(ctx, ss)
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
return handler(srv, wrapped)
}
return interceptor, nil
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
// dismantleToken extracts the user and scopes from the reva access token
func dismantleToken(ctx context.Context, tkn string, req interface{}, mgr token.Manager, gatewayAddr string) (*userpb.User, map[string]*authpb.Scope, error) {
u, tokenScope, err := mgr.DismantleToken(ctx, tkn)
if err != nil {
return nil, nil, err
}
if sharedconf.SkipUserGroupsInToken() {
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return nil, nil, err
}
groups, err := getUserGroups(ctx, u, client)
if err != nil {
return nil, nil, err
}
u.Groups = groups
}
// Check if access to the resource is in the scope of the token
ok, err := scope.VerifyScope(ctx, tokenScope, req)
if err != nil {
return nil, nil, errtypes.InternalError("error verifying scope of access token")
}
if ok {
return u, tokenScope, nil
}
if err = expandAndVerifyScope(ctx, req, tokenScope, u, gatewayAddr, mgr); err != nil {
return nil, nil, err
}
return u, tokenScope, nil
}
func getUserGroups(ctx context.Context, u *userpb.User, client gatewayv1beta1.GatewayAPIClient) ([]string, error) {
if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil {
log := appctx.GetLogger(ctx)
log.Info().Str("userid", u.Id.OpaqueId).Msg("user groups found in cache")
return groupsIf.([]string), nil
}
res, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserGroups")
}
_ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, res.Groups, 3600*time.Second)
return res.Groups, nil
}
@@ -0,0 +1,464 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package auth
import (
"context"
"fmt"
"path/filepath"
"strings"
"time"
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"
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"
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/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
statuspkg "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/grpc/metadata"
)
const (
scopeDelimiter = "#"
scopeCacheExpiration = 3600
)
func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[string]*authpb.Scope, user *userpb.User, gatewayAddr string, mgr token.Manager) error {
log := appctx.GetLogger(ctx)
client, err := pool.GetGatewayServiceClient(gatewayAddr)
if err != nil {
return err
}
if ref, ok := extractRef(req, tokenScope); ok {
// The request is for a storage reference. This can be the case for multiple scenarios:
// - If the path is not empty, the request might be coming from a share where the accessor is
// trying to impersonate the owner, since the share manager doesn't know the
// share path.
// - If the ID not empty, the request might be coming from
// - a resource present inside a shared folder, or
// - a share created for a lightweight account after the token was minted.
log.Info().Msgf("resolving storage reference to check token scope %s", ref.String())
for k := range tokenScope {
switch {
case strings.HasPrefix(k, "publicshare"):
if err = resolvePublicShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "lightweight"):
if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil {
return nil
}
case strings.HasPrefix(k, "ocmshare"):
if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil {
return nil
}
}
log.Err(err).Interface("ref", ref).Interface("scope", k).Msg("error resolving reference under scope")
}
} else if ref, ok := extractShareRef(req); ok {
// It's a share ref
// The request might be coming from a share created for a lightweight account
// after the token was minted.
log.Info().Msgf("resolving share reference against received shares to verify token scope %+v", ref.String())
for k := range tokenScope {
if strings.HasPrefix(k, "lightweight") {
// Check if this ID is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + ref.GetId().OpaqueId
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
log.Warn().Err(err).Msg("error listing received shares")
continue
}
for _, s := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + s.Share.Id.OpaqueId
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
if ref.GetId() != nil && ref.GetId().OpaqueId == s.Share.Id.OpaqueId {
return nil
}
if key := ref.GetKey(); key != nil && (utils.UserEqual(key.Owner, s.Share.Owner) || utils.UserEqual(key.Owner, s.Share.Creator)) &&
utils.ResourceIDEqual(key.ResourceId, s.Share.ResourceId) && utils.GranteeEqual(key.Grantee, s.Share.Grantee) {
return nil
}
}
}
}
}
return errtypes.PermissionDenied(fmt.Sprintf("access to resource %+v not allowed within the assigned scope", req))
}
func resolveLightweightScope(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, user *userpb.User, client gateway.GatewayAPIClient, mgr token.Manager) error {
refString, err := storagespace.FormatReference(ref)
if err != nil {
// cannot format reference, so cannot be valid
return errtypes.PermissionDenied("invalid reference")
}
// Check if this ref is cached
key := "lw:" + user.Id.OpaqueId + scopeDelimiter + refString
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
shares, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil || shares.Status.Code != rpc.Code_CODE_OK {
return errtypes.InternalError("error listing received shares")
}
for _, share := range shares.Shares {
shareKey := "lw:" + user.Id.OpaqueId + scopeDelimiter + storagespace.FormatResourceID(share.Share.ResourceId)
_ = scopeExpansionCache.SetWithExpire(shareKey, nil, scopeCacheExpiration*time.Second)
if ref.ResourceId != nil && utils.ResourceIDEqual(share.Share.ResourceId, ref.ResourceId) {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, share.Share.ResourceId, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}
}
return errtypes.PermissionDenied("request is not for a nested resource")
}
func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error {
var share link.PublicShare
err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share)
if err != nil {
return err
}
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
}
// for ListOCMSharesRequest, the ref resource id is empty and we set path to . to indicate the root of the share
if ref.GetResourceId() == nil && ref.Path == "." {
ref.ResourceId = share.GetResourceId()
}
return checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr)
}
func checkCacheForNestedResource(ctx context.Context, ref *provider.Reference, resource *provider.ResourceId, client gateway.GatewayAPIClient, mgr token.Manager) error {
refString, err := storagespace.FormatReference(ref)
if err != nil {
// cannot format reference, so cannot be valid
return errtypes.PermissionDenied("invalid reference")
}
// Check if this ref is cached
key := storagespace.FormatResourceID(resource) + scopeDelimiter + refString
if _, err := scopeExpansionCache.Get(key); err == nil {
return nil
}
if ok, err := checkIfNestedResource(ctx, ref, resource, client, mgr); err == nil && ok {
_ = scopeExpansionCache.SetWithExpire(key, nil, scopeCacheExpiration*time.Second)
return nil
}
return errtypes.PermissionDenied("request is not for a nested resource")
}
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.
statResponse, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: parent}})
if err != nil {
return false, err
}
if statResponse.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(statResponse.Status.Code, "auth interceptor")
}
parentInfo := statResponse.GetInfo()
// We need to find out if the requested resource is child of the `parent` (coming from token scope)
// 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
// NOTE: did somebody say service accounts? ...
var user *userpb.User
if parentInfo.GetOwner().GetType() == userpb.UserType_USER_TYPE_SPACE_OWNER {
// fake a space owner user
user = &userpb.User{
Id: parentInfo.GetOwner(),
}
} else {
userResp, err := client.GetUser(ctx, &userpb.GetUserRequest{UserId: parentInfo.GetOwner(), SkipFetchingUserGroups: true})
if err != nil || userResp.Status.Code != rpc.Code_CODE_OK {
return false, err
}
user = userResp.User
}
scope, err := scope.AddOwnerScope(map[string]*authpb.Scope{})
if err != nil {
return false, err
}
token, err := mgr.MintToken(ctx, user, scope)
if err != nil {
return false, err
}
ctx = metadata.AppendToOutgoingContext(context.Background(), ctxpkg.TokenHeader, token)
childStat, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
return false, err
}
if childStat.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && ref.GetPath() != "" && ref.GetPath() != "." {
// The resource does not seem to exist (yet?). We might be part of an initiate upload request.
// Stat the parent to get its path and check that against the root path.
childStat, err = client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ref.GetResourceId()}})
if err != nil {
return false, err
}
}
if childStat.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(childStat.Status.Code, "auth interceptor")
}
childInfo := childStat.GetInfo()
// child can only be a nested resource if it is within the same space as parent
if childInfo.GetId().GetStorageId() != parentInfo.GetId().GetStorageId() ||
childInfo.GetId().GetSpaceId() != parentInfo.GetId().GetSpaceId() {
return false, nil
}
// Both resources are in the same space, now check paths
pathResp, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: statResponse.GetInfo().GetId()})
if err != nil {
return false, err
}
if pathResp.Status.Code != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
}
parentPath := pathResp.Path
pathResp, err = client.GetPath(ctx, &provider.GetPathRequest{ResourceId: childStat.GetInfo().GetId()})
if err != nil {
return false, err
}
if pathResp.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, statuspkg.NewErrorFromCode(pathResp.Status.Code, "auth interceptor")
}
childPath := pathResp.Path
rel, err := filepath.Rel(parentPath, childPath)
if err != nil {
return false, err
}
return !strings.HasPrefix(rel, ".."), nil
}
func extractRefFromListProvidersReq(v *registry.ListStorageProvidersRequest) (*provider.Reference, bool) {
ref := &provider.Reference{}
if v.Opaque != nil && v.Opaque.Map != nil {
if e, ok := v.Opaque.Map["storage_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.StorageId = string(e.Value)
}
if e, ok := v.Opaque.Map["space_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.SpaceId = string(e.Value)
}
if e, ok := v.Opaque.Map["opaque_id"]; ok {
if ref.ResourceId == nil {
ref.ResourceId = &provider.ResourceId{}
}
ref.ResourceId.OpaqueId = string(e.Value)
}
if e, ok := v.Opaque.Map["path"]; ok {
ref.Path = string(e.Value)
}
}
return ref, true
}
func extractRefForReaderRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Read requests
case *registry.GetStorageProvidersRequest:
return v.GetRef(), true
case *registry.ListStorageProvidersRequest:
return extractRefFromListProvidersReq(v)
case *provider.StatRequest:
return v.GetRef(), true
case *provider.ListContainerRequest:
return v.GetRef(), true
case *provider.InitiateFileDownloadRequest:
return v.GetRef(), true
// App provider requests
case *appregistry.GetAppProvidersRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *appprovider.OpenInAppRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *gateway.OpenInAppRequest:
return v.GetRef(), true
// Locking
case *provider.GetLockRequest:
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
// OCM shares
case *ocmv1beta1.ListReceivedOCMSharesRequest:
return &provider.Reference{Path: "."}, true // we will try to stat the shared node
}
return nil, false
}
func extractRefForUploaderRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Write Requests
case *registry.GetStorageProvidersRequest:
return v.GetRef(), true
case *registry.ListStorageProvidersRequest:
return extractRefFromListProvidersReq(v)
case *provider.StatRequest:
return v.GetRef(), true
case *provider.CreateContainerRequest:
return v.GetRef(), true
case *provider.TouchFileRequest:
return v.GetRef(), true
case *provider.InitiateFileUploadRequest:
return v.GetRef(), true
// App provider requests
case *appregistry.GetAppProvidersRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *appprovider.OpenInAppRequest:
return &provider.Reference{ResourceId: v.ResourceInfo.Id}, true
case *gateway.OpenInAppRequest:
return v.GetRef(), true
// Locking
case *provider.GetLockRequest:
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
}
func extractRefForEditorRole(req interface{}) (*provider.Reference, bool) {
switch v := req.(type) {
// Remaining edit Requests
case *provider.DeleteRequest:
return v.GetRef(), true
case *provider.MoveRequest:
return v.GetSource(), true
case *provider.SetArbitraryMetadataRequest:
return v.GetRef(), true
case *provider.UnsetArbitraryMetadataRequest:
return v.GetRef(), true
}
return nil, false
}
func extractRef(req interface{}, tokenScope map[string]*authpb.Scope) (*provider.Reference, bool) {
var readPerm, uploadPerm, editPerm bool
for _, v := range tokenScope {
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_VIEWER {
readPerm = true
}
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR || v.Role == authpb.Role_ROLE_UPLOADER {
uploadPerm = true
}
if v.Role == authpb.Role_ROLE_OWNER || v.Role == authpb.Role_ROLE_EDITOR {
editPerm = true
}
}
if readPerm {
ref, ok := extractRefForReaderRole(req)
if ok {
return ref, true
}
}
if uploadPerm {
ref, ok := extractRefForUploaderRole(req)
if ok {
return ref, true
}
}
if editPerm {
ref, ok := extractRefForEditorRole(req)
if ok {
return ref, true
}
}
return nil, false
}
func extractShareRef(req interface{}) (*collaboration.ShareReference, bool) {
switch v := req.(type) {
case *collaboration.GetReceivedShareRequest:
return v.GetRef(), true
case *collaboration.UpdateReceivedShareRequest:
return &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: v.GetShare().GetShare().GetId()}}, true
}
return nil, false
}
@@ -0,0 +1,496 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package eventsmiddleware
import (
"strings"
"time"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/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"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// ContainerCreated converts the response to an event
func ContainerCreated(r *provider.CreateContainerResponse, req *provider.CreateContainerRequest, spaceOwner *user.UserId, executant *user.User) events.ContainerCreated {
return events.ContainerCreated{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ShareCreated converts the response to an event
func ShareCreated(r *collaboration.CreateShareResponse, executant *user.User) events.ShareCreated {
return events.ShareCreated{
ShareID: r.Share.GetId(),
Executant: executant.GetId(),
Sharer: r.Share.Creator,
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
CTime: r.Share.Ctime,
Permissions: r.Share.Permissions,
}
}
// ShareRemoved converts the response to an event
func ShareRemoved(r *collaboration.RemoveShareResponse, req *collaboration.RemoveShareRequest, executant *user.User) events.ShareRemoved {
var (
userid *user.UserId
groupid *group.GroupId
rid *provider.ResourceId
)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteeuserid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteegroupid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
return events.ShareRemoved{
Executant: executant.GetId(),
ShareID: req.Ref.GetId(),
ShareKey: req.Ref.GetKey(),
GranteeUserID: userid,
GranteeGroupID: groupid,
ItemID: rid,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Timestamp: time.Now(),
}
}
// ShareUpdated converts the response to an event
func ShareUpdated(r *collaboration.UpdateShareResponse, req *collaboration.UpdateShareRequest, executant *user.User) events.ShareUpdated {
return events.ShareUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
Sharer: r.Share.Creator,
MTime: r.Share.Mtime,
UpdateMask: req.GetUpdateMask().GetPaths(),
}
}
// ReceivedShareUpdated converts the response to an event
func ReceivedShareUpdated(r *collaboration.UpdateReceivedShareResponse, executant *user.User) events.ReceivedShareUpdated {
return events.ReceivedShareUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Share.Id,
ItemID: r.Share.Share.ResourceId,
Permissions: r.Share.Share.Permissions,
GranteeUserID: r.Share.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.Share.GetGrantee().GetGroupId(),
Sharer: r.Share.Share.Creator,
MTime: r.Share.Share.Mtime,
State: collaboration.ShareState_name[int32(r.Share.State)],
}
}
// LinkCreated converts the response to an event
func LinkCreated(r *link.CreatePublicShareResponse, executant *user.User) events.LinkCreated {
return events.LinkCreated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
CTime: r.Share.Ctime,
Token: r.Share.Token,
}
}
// LinkUpdated converts the response to an event
func LinkUpdated(r *link.UpdatePublicShareResponse, req *link.UpdatePublicShareRequest, executant *user.User) events.LinkUpdated {
return events.LinkUpdated{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
MTime: r.Share.Mtime,
Token: r.Share.Token,
FieldUpdated: link.UpdatePublicShareRequest_Update_Type_name[int32(req.Update.GetType())],
}
}
// LinkAccessed converts the response to an event
func LinkAccessed(r *link.GetPublicShareByTokenResponse, executant *user.User) events.LinkAccessed {
return events.LinkAccessed{
Executant: executant.GetId(),
ShareID: r.Share.Id,
Sharer: r.Share.Creator,
ItemID: r.Share.ResourceId,
Permissions: r.Share.Permissions,
DisplayName: r.Share.DisplayName,
Expiration: r.Share.Expiration,
PasswordProtected: r.Share.PasswordProtected,
CTime: r.Share.Ctime,
Token: r.Share.Token,
}
}
// LinkAccessFailed converts the response to an event
func LinkAccessFailed(r *link.GetPublicShareByTokenResponse, req *link.GetPublicShareByTokenRequest, executant *user.User) events.LinkAccessFailed {
e := events.LinkAccessFailed{
Executant: executant.GetId(),
Status: r.Status.Code,
Message: r.Status.Message,
Timestamp: utils.TSNow(),
Token: req.Token,
}
if r.Share != nil {
e.ShareID = r.Share.Id
e.Token = r.Share.Token
}
return e
}
// LinkRemoved converts the response to an event
func LinkRemoved(r *link.RemovePublicShareResponse, req *link.RemovePublicShareRequest, executant *user.User) events.LinkRemoved {
var rid *provider.ResourceId
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
return events.LinkRemoved{
Executant: executant.GetId(),
ShareID: req.Ref.GetId(),
ShareToken: req.Ref.GetToken(),
Timestamp: utils.TSNow(),
ItemID: rid,
ResourceName: utils.ReadPlainFromOpaque(r.Opaque, "resourcename"),
}
}
func OCMCoreShareCreated(r *ocmcore.CreateOCMCoreShareResponse, req *ocmcore.CreateOCMCoreShareRequest, executant *user.User) events.OCMCoreShareCreated {
var permissions *provider.ResourcePermissions
for _, p := range req.GetProtocols() {
if p.GetWebdavOptions() != nil {
permissions = p.GetWebdavOptions().GetPermissions().GetPermissions()
break
}
}
return events.OCMCoreShareCreated{
ShareID: r.GetId(),
Executant: executant.GetId(),
Sharer: req.GetSender(),
GranteeUserID: req.GetShareWith(),
ItemID: req.GetResourceId(),
ResourceName: req.GetName(),
CTime: r.GetCreated(),
Permissions: permissions,
}
}
// FileTouched converts the response to an event
func FileTouched(r *provider.TouchFileResponse, req *provider.TouchFileRequest, spaceOwner *user.UserId, executant *user.User) events.FileTouched {
return events.FileTouched{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileUploaded converts the response to an event
func FileUploaded(r *provider.InitiateFileUploadResponse, req *provider.InitiateFileUploadRequest, spaceOwner *user.UserId, executant *user.User) events.FileUploaded {
return events.FileUploaded{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileDownloaded converts the response to an event
func FileDownloaded(r *provider.InitiateFileDownloadResponse, req *provider.InitiateFileDownloadRequest, executant *user.User) events.FileDownloaded {
return events.FileDownloaded{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileLocked converts the response to an events
func FileLocked(r *provider.SetLockResponse, req *provider.SetLockRequest, owner *user.UserId, executant *user.User) events.FileLocked {
return events.FileLocked{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileUnlocked converts the response to an event
func FileUnlocked(r *provider.UnlockResponse, req *provider.UnlockRequest, owner *user.UserId, executant *user.User) events.FileUnlocked {
return events.FileUnlocked{
Executant: executant.GetId(),
Ref: req.Ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemTrashed converts the response to an event
func ItemTrashed(r *provider.DeleteResponse, req *provider.DeleteRequest, spaceOwner *user.UserId, executant *user.User) events.ItemTrashed {
opaqueID := utils.ReadPlainFromOpaque(r.Opaque, "opaque_id")
return events.ItemTrashed{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
ID: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: opaqueID,
},
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemMoved converts the response to an event
func ItemMoved(r *provider.MoveResponse, req *provider.MoveRequest, spaceOwner *user.UserId, executant *user.User) events.ItemMoved {
return events.ItemMoved{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Destination,
OldReference: req.Source,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// TrashbinPurged converts the response to an event
func TrashbinPurged(r *provider.PurgeRecycleResponse, req *provider.PurgeRecycleRequest, executant *user.User) events.TrashbinPurged {
return events.TrashbinPurged{
Executant: executant.GetId(),
Ref: &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: req.Ref.GetResourceId().GetSpaceId(),
},
},
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemPurged converts the response to an event
func ItemPurged(r *provider.PurgeRecycleResponse, req *provider.PurgeRecycleRequest, executant *user.User) events.ItemPurged {
root, relativePath := router.ShiftPath(req.Key)
if relativePath == "/" && !strings.HasSuffix(req.Key, "/") {
relativePath = ""
}
if root == "" {
// if there is no key this is about purging the whole trashbin
root = req.Ref.GetResourceId().GetSpaceId()
}
ref := &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: req.Ref.GetResourceId().GetStorageId(),
SpaceId: req.Ref.GetResourceId().GetSpaceId(),
OpaqueId: root,
},
Path: relativePath,
}
return events.ItemPurged{
Executant: executant.GetId(),
Ref: ref,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// ItemRestored converts the response to an event
func ItemRestored(r *provider.RestoreRecycleItemResponse, req *provider.RestoreRecycleItemRequest, spaceOwner *user.UserId, executant *user.User) events.ItemRestored {
ref := req.Ref
if req.RestoreRef != nil {
ref = req.RestoreRef
}
return events.ItemRestored{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: ref,
OldReference: req.Ref,
Key: req.Key,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// FileVersionRestored converts the response to an event
func FileVersionRestored(r *provider.RestoreFileVersionResponse, req *provider.RestoreFileVersionRequest, spaceOwner *user.UserId, executant *user.User) events.FileVersionRestored {
return events.FileVersionRestored{
SpaceOwner: spaceOwner,
Executant: executant.GetId(),
Ref: req.Ref,
Key: req.Key,
Timestamp: utils.TSNow(),
ImpersonatingUser: extractImpersonator(executant),
}
}
// SpaceCreated converts the response to an event
func SpaceCreated(r *provider.CreateStorageSpaceResponse, executant *user.User) events.SpaceCreated {
return events.SpaceCreated{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Root: r.StorageSpace.Root,
Name: r.StorageSpace.Name,
Type: r.StorageSpace.SpaceType,
Quota: r.StorageSpace.Quota,
MTime: r.StorageSpace.Mtime,
}
}
// SpaceRenamed converts the response to an event
func SpaceRenamed(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceRenamed {
return events.SpaceRenamed{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Name: r.StorageSpace.Name,
Timestamp: utils.TSNow(),
}
}
// SpaceUpdated converts the response to an event
func SpaceUpdated(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceUpdated {
return events.SpaceUpdated{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Space: r.StorageSpace,
Timestamp: utils.TSNow(),
}
}
// SpaceEnabled converts the response to an event
func SpaceEnabled(r *provider.UpdateStorageSpaceResponse, req *provider.UpdateStorageSpaceRequest, executant *user.User) events.SpaceEnabled {
return events.SpaceEnabled{
Executant: executant.GetId(),
ID: r.StorageSpace.Id,
Owner: extractOwner(r.StorageSpace.Owner),
Timestamp: utils.TSNow(),
}
}
// SpaceShared converts the response to an event
func SpaceShared(r *collaboration.CreateShareResponse, executant *user.User) events.SpaceShared {
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
return events.SpaceShared{
Executant: executant.GetId(),
Creator: r.Share.GetCreator(),
GranteeUserID: r.Share.GetGrantee().GetUserId(),
GranteeGroupID: r.Share.GetGrantee().GetGroupId(),
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceShareUpdated converts the response to an events
func SpaceShareUpdated(r *collaboration.UpdateShareResponse, executant *user.User) events.SpaceShareUpdated {
id := storagespace.FormatStorageID(r.GetShare().GetResourceId().GetStorageId(), r.GetShare().GetResourceId().GetSpaceId())
return events.SpaceShareUpdated{
Executant: executant.GetId(),
GranteeUserID: r.GetShare().GetGrantee().GetUserId(),
GranteeGroupID: r.GetShare().GetGrantee().GetGroupId(),
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceUnshared converts the response to an event
func SpaceUnshared(r *collaboration.RemoveShareResponse, req *collaboration.RemoveShareRequest, executant *user.User) events.SpaceUnshared {
var (
userid *user.UserId
groupid *group.GroupId
rid *provider.ResourceId
)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteeuserid", &userid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "granteegroupid", &groupid)
_ = utils.ReadJSONFromOpaque(r.Opaque, "resourceid", &rid)
id := storagespace.FormatStorageID(rid.StorageId, rid.SpaceId)
return events.SpaceUnshared{
Executant: executant.GetId(),
GranteeUserID: userid,
GranteeGroupID: groupid,
ID: &provider.StorageSpaceId{OpaqueId: id},
Timestamp: time.Now(),
}
}
// SpaceDisabled converts the response to an event
func SpaceDisabled(r *provider.DeleteStorageSpaceResponse, req *provider.DeleteStorageSpaceRequest, executant *user.User) events.SpaceDisabled {
return events.SpaceDisabled{
Executant: executant.GetId(),
ID: req.Id,
Timestamp: time.Now(),
}
}
// SpaceDeleted converts the response to an event
func SpaceDeleted(r *provider.DeleteStorageSpaceResponse, req *provider.DeleteStorageSpaceRequest, executant *user.User) events.SpaceDeleted {
var final map[string]provider.ResourcePermissions
_ = utils.ReadJSONFromOpaque(r.GetOpaque(), "grants", &final)
return events.SpaceDeleted{
Executant: executant.GetId(),
ID: req.Id,
SpaceName: utils.ReadPlainFromOpaque(r.GetOpaque(), "spacename"),
FinalMembers: final,
Timestamp: time.Now(),
}
}
func extractOwner(u *user.User) *user.UserId {
if u != nil {
return u.Id
}
return nil
}
func extractImpersonator(u *user.User) *user.User {
var impersonator user.User
if err := utils.ReadJSONFromOpaque(u.Opaque, "impersonating-user", &impersonator); err != nil {
return nil
}
return &impersonator
}
@@ -0,0 +1,246 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package eventsmiddleware
import (
"context"
"fmt"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
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"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/grpc"
)
const (
defaultPriority = 200
)
func init() {
rgrpc.RegisterUnaryInterceptor("eventsmiddleware", NewUnary)
}
// NewUnary returns a new unary interceptor that emits events when needed
// no lint because of the switch statement that should be extendable
//
//nolint:gocritic
func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
publisher, err := publisherFromConfig(m)
if err != nil {
return nil, 0, err
}
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Register a channel in the context to receive the space owner id from the handler(s) further down the stack
var ownerID *user.UserId
sendOwnerChan := make(chan *user.UserId)
ctx = storagespace.ContextRegisterSendOwnerChan(ctx, sendOwnerChan)
res, err := handler(ctx, req)
if err != nil {
return res, err
}
// Read the space owner id from the channel
select {
case ownerID = <-sendOwnerChan:
default:
}
executant, _ := revactx.ContextGetUser(ctx)
// The MoveResponse event is moved to the decomposedfs
var ev interface{}
switch v := res.(type) {
case *collaboration.CreateShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "noevent") {
break
}
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceShared(v, executant)
} else {
ev = ShareCreated(v, executant)
}
}
case *collaboration.RemoveShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceUnshared(v, req.(*collaboration.RemoveShareRequest), executant)
} else {
ev = ShareRemoved(v, req.(*collaboration.RemoveShareRequest), executant)
}
}
case *collaboration.UpdateShareResponse:
if isSuccess(v) {
if utils.ExistsInOpaque(v.Opaque, "spacegrant") {
ev = SpaceShareUpdated(v, executant)
} else {
ev = ShareUpdated(v, req.(*collaboration.UpdateShareRequest), executant)
}
}
case *collaboration.UpdateReceivedShareResponse:
if isSuccess(v) {
ev = ReceivedShareUpdated(v, executant)
}
case *link.CreatePublicShareResponse:
if isSuccess(v) {
ev = LinkCreated(v, executant)
}
case *link.UpdatePublicShareResponse:
if isSuccess(v) {
ev = LinkUpdated(v, req.(*link.UpdatePublicShareRequest), executant)
}
case *link.RemovePublicShareResponse:
if isSuccess(v) {
ev = LinkRemoved(v, req.(*link.RemovePublicShareRequest), executant)
}
case *link.GetPublicShareByTokenResponse:
if isSuccess(v) {
ev = LinkAccessed(v, executant)
} else {
ev = LinkAccessFailed(v, req.(*link.GetPublicShareByTokenRequest), executant)
}
case *ocmcore.CreateOCMCoreShareResponse:
if isSuccess(v) {
ev = OCMCoreShareCreated(v, req.(*ocmcore.CreateOCMCoreShareRequest), executant)
}
case *provider.CreateContainerResponse:
if isSuccess(v) {
ev = ContainerCreated(v, req.(*provider.CreateContainerRequest), ownerID, executant)
}
case *provider.InitiateFileDownloadResponse:
if isSuccess(v) {
ev = FileDownloaded(v, req.(*provider.InitiateFileDownloadRequest), executant)
}
case *provider.DeleteResponse:
if isSuccess(v) {
ev = ItemTrashed(v, req.(*provider.DeleteRequest), ownerID, executant)
}
case *provider.PurgeRecycleResponse:
if isSuccess(v) {
id := req.(*provider.PurgeRecycleRequest).GetRef().GetResourceId()
if id != nil && id.OpaqueId == id.SpaceId && req.(*provider.PurgeRecycleRequest).Key == "" {
// this is a purge of the whole trashbin, NOT the whole space
ev = TrashbinPurged(v, req.(*provider.PurgeRecycleRequest), executant)
} else {
ev = ItemPurged(v, req.(*provider.PurgeRecycleRequest), executant)
}
}
case *provider.RestoreRecycleItemResponse:
if isSuccess(v) {
ev = ItemRestored(v, req.(*provider.RestoreRecycleItemRequest), ownerID, executant)
}
case *provider.RestoreFileVersionResponse:
if isSuccess(v) {
ev = FileVersionRestored(v, req.(*provider.RestoreFileVersionRequest), ownerID, executant)
}
case *provider.CreateStorageSpaceResponse:
if isSuccess(v) && v.StorageSpace != nil { // TODO: Why are there CreateStorageSpaceResponses with nil StorageSpace?
ev = SpaceCreated(v, executant)
}
case *provider.UpdateStorageSpaceResponse:
if isSuccess(v) {
r := req.(*provider.UpdateStorageSpaceRequest)
if r.StorageSpace.Name != "" {
ev = SpaceRenamed(v, r, executant)
} else if utils.ExistsInOpaque(r.Opaque, "restore") {
ev = SpaceEnabled(v, r, executant)
} else {
ev = SpaceUpdated(v, r, executant)
}
}
case *provider.DeleteStorageSpaceResponse:
if isSuccess(v) {
r := req.(*provider.DeleteStorageSpaceRequest)
if utils.ExistsInOpaque(r.Opaque, "purge") {
ev = SpaceDeleted(v, r, executant)
} else {
ev = SpaceDisabled(v, r, executant)
}
}
case *provider.TouchFileResponse:
if isSuccess(v) {
ev = FileTouched(v, req.(*provider.TouchFileRequest), ownerID, executant)
}
case *provider.SetLockResponse:
if isSuccess(v) {
ev = FileLocked(v, req.(*provider.SetLockRequest), ownerID, executant)
}
case *provider.UnlockResponse:
if isSuccess(v) {
ev = FileUnlocked(v, req.(*provider.UnlockRequest), ownerID, executant)
}
}
if ev != nil {
if err := events.Publish(ctx, publisher, ev); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("event", ev).Msg("publishing event failed")
}
}
return res, nil
}
return interceptor, defaultPriority, nil
}
// NewStream returns a new server stream interceptor
// that creates the application context.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// TODO: Use ss.RecvMsg() and ss.SendMsg() to send events from a stream
return handler(srv, ss)
}
return interceptor
}
// common interface to all responses
type su interface {
GetStatus() *rpc.Status
}
func isSuccess(res su) bool {
return res.GetStatus().Code == rpc.Code_CODE_OK
}
func publisherFromConfig(m map[string]interface{}) (events.Publisher, error) {
typ := m["type"].(string)
switch typ {
default:
return nil, fmt.Errorf("stream type '%s' not supported", typ)
case "nats":
var cfg stream.NatsConfig
if err := mapstructure.Decode(m, &cfg); err != nil {
return nil, err
}
name, _ := m["name"].(string)
return stream.NatsFromConfig(name, false, cfg)
}
}
@@ -0,0 +1,27 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core GRPC services
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/eventsmiddleware"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/interceptors/readonly"
// Add your own service here
)
@@ -0,0 +1,117 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package log
import (
"context"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
)
// NewUnary returns a new unary interceptor
// that logs grpc calls.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
res, err := handler(ctx, req)
code := status.Code(err)
end := time.Now()
diff := end.Sub(start).Nanoseconds()
var fromAddress string
if p, ok := peer.FromContext(ctx); ok {
fromAddress = p.Addr.Network() + "://" + p.Addr.String()
}
userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx)
if !ok {
userAgent = ""
}
log := appctx.GetLogger(ctx)
var event *zerolog.Event
var msg string
if code != codes.OK {
event = log.Error()
msg = err.Error()
} else {
event = log.Debug()
msg = "unary"
}
event.Str("user-agent", userAgent).
Str("from", fromAddress).
Str("uri", info.FullMethod).
Str("start", start.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Str("code", code.String()).
Msg(msg)
return res, err
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
start := time.Now()
err := handler(srv, ss)
end := time.Now()
code := status.Code(err)
diff := end.Sub(start).Nanoseconds()
var fromAddress string
if p, ok := peer.FromContext(ss.Context()); ok {
fromAddress = p.Addr.Network() + "://" + p.Addr.String()
}
userAgent, ok := ctxpkg.ContextGetUserAgentString(ctx)
if !ok {
userAgent = ""
}
log := appctx.GetLogger(ss.Context())
var event *zerolog.Event
var msg string
if code != codes.OK {
event = log.Error()
msg = err.Error()
} else {
event = log.Debug()
msg = "stream"
}
event.Str("user-agent", userAgent).
Str("from", fromAddress).
Str("uri", info.FullMethod).
Str("start", start.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Str("code", code.String()).
Msg(msg)
return err
}
return interceptor
}
@@ -0,0 +1,77 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package prometheus
import (
"context"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"google.golang.org/grpc"
)
const (
defaultPriority = 100
)
func init() {
rgrpc.RegisterUnaryInterceptor("prometheus", NewUnary)
}
// NewUnary returns a new unary interceptor
// that counts grpc calls.
func NewUnary(m map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
interceptor, err := interceptorFromConfig(m)
if err != nil {
return nil, 0, err
}
return interceptor, defaultPriority, nil
}
// NewStream returns a new server stream interceptor
// that counts grpc calls.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := handler(srv, ss)
// TODO count res codes & errors
return err
}
return interceptor
}
func interceptorFromConfig(m map[string]interface{}) (grpc.UnaryServerInterceptor, error) {
namespace := m["namespace"].(string)
if namespace == "" {
namespace = "reva"
}
subsystem := m["subsystem"].(string)
reqProcessed := promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "grpc_requests_total",
Help: "The total number of processed " + subsystem + " GRPC requests for " + namespace,
})
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
res, err := handler(ctx, req)
reqProcessed.Inc()
return res, err
}
return interceptor, nil
}
@@ -0,0 +1,150 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package readonly
import (
"context"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
defaultPriority = 200
)
func init() {
rgrpc.RegisterUnaryInterceptor("readonly", NewUnary)
}
// NewUnary returns a new unary interceptor
// that checks grpc calls and blocks write requests.
func NewUnary(map[string]interface{}) (grpc.UnaryServerInterceptor, int, error) {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log := appctx.GetLogger(ctx)
switch req.(type) {
// handle known non-write request types
case *provider.GetHomeRequest,
*provider.GetPathRequest,
*provider.GetQuotaRequest,
*registry.GetStorageProvidersRequest,
*provider.InitiateFileDownloadRequest,
*provider.ListFileVersionsRequest,
*provider.ListGrantsRequest,
*provider.ListRecycleRequest:
return handler(ctx, req)
case *provider.ListContainerRequest:
resp, err := handler(ctx, req)
if listResp, ok := resp.(*provider.ListContainerResponse); ok && listResp.Infos != nil {
for _, info := range listResp.Infos {
// use the existing PermissionsSet and change the writes to false
if info.PermissionSet != nil {
info.PermissionSet.AddGrant = false
info.PermissionSet.CreateContainer = false
info.PermissionSet.Delete = false
info.PermissionSet.InitiateFileUpload = false
info.PermissionSet.Move = false
info.PermissionSet.RemoveGrant = false
info.PermissionSet.PurgeRecycle = false
info.PermissionSet.RestoreFileVersion = false
info.PermissionSet.RestoreRecycleItem = false
info.PermissionSet.UpdateGrant = false
}
}
}
return resp, err
case *provider.StatRequest:
resp, err := handler(ctx, req)
if statResp, ok := resp.(*provider.StatResponse); ok && statResp.Info != nil && statResp.Info.PermissionSet != nil {
// use the existing PermissionsSet and change the writes to false
statResp.Info.PermissionSet.AddGrant = false
statResp.Info.PermissionSet.CreateContainer = false
statResp.Info.PermissionSet.Delete = false
statResp.Info.PermissionSet.InitiateFileUpload = false
statResp.Info.PermissionSet.Move = false
statResp.Info.PermissionSet.RemoveGrant = false
statResp.Info.PermissionSet.PurgeRecycle = false
statResp.Info.PermissionSet.RestoreFileVersion = false
statResp.Info.PermissionSet.RestoreRecycleItem = false
statResp.Info.PermissionSet.UpdateGrant = false
}
return resp, err
// Don't allow the following requests types
case *provider.AddGrantRequest:
return &provider.AddGrantResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to add grant on readonly storage"),
}, nil
case *provider.CreateContainerRequest:
return &provider.CreateContainerResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create resource on read-only storage"),
}, nil
case *provider.TouchFileRequest:
return &provider.TouchFileResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create resource on read-only storage"),
}, nil
case *provider.CreateHomeRequest:
return &provider.CreateHomeResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to create home on readonly storage"),
}, nil
case *provider.DeleteRequest:
return &provider.DeleteResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to delete resource on readonly storage"),
}, nil
case *provider.InitiateFileUploadRequest:
return &provider.InitiateFileUploadResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to upload resource on readonly storage"),
}, nil
case *provider.MoveRequest:
return &provider.MoveResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to move resource on readonly storage"),
}, nil
case *provider.PurgeRecycleRequest:
return &provider.PurgeRecycleResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to purge recycle on readonly storage"),
}, nil
case *provider.RemoveGrantRequest:
return &provider.RemoveGrantResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to remove grant on readonly storage"),
}, nil
case *provider.RestoreRecycleItemRequest:
return &provider.RestoreRecycleItemResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to restore recycle item on readonly storage"),
}, nil
case *provider.SetArbitraryMetadataRequest:
return &provider.SetArbitraryMetadataResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to set arbitrary metadata on readonly storage"),
}, nil
case *provider.UnsetArbitraryMetadataRequest:
return &provider.UnsetArbitraryMetadataResponse{
Status: rstatus.NewPermissionDenied(ctx, nil, "permission denied: tried to unset arbitrary metadata on readonly storage"),
}, nil
// block unknown request types and return error
default:
log.Debug().Msg("storage is readonly")
return nil, status.Errorf(codes.PermissionDenied, "permission denied: tried to execute an unknown operation: %T!", req)
}
}, defaultPriority, nil
}
@@ -0,0 +1,52 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package recovery
import (
"context"
"runtime/debug"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// NewUnary returns a server interceptor that adds telemetry to
// grpc calls.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(recoveryFunc))
return interceptor
}
// NewStream returns a streaming server interceptor that adds telemetry to
// streaming grpc calls.
func NewStream() grpc.StreamServerInterceptor {
interceptor := grpc_recovery.StreamServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(recoveryFunc))
return interceptor
}
func recoveryFunc(ctx context.Context, p interface{}) (err error) {
debug.PrintStack()
log := appctx.GetLogger(ctx)
log.Error().Msgf("%+v; stack: %s", p, debug.Stack())
return status.Errorf(codes.Internal, "%s", p)
}
@@ -0,0 +1,99 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package token
import (
"context"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// NewUnary returns a new unary interceptor that adds
// the token to the context.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok && md != nil {
if val, ok := md[ctxpkg.TokenHeader]; ok {
if len(val) > 0 && val[0] != "" {
tkn := val[0]
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
}
}
if val, ok := md[ctxpkg.InitiatorHeader]; ok {
if len(val) > 0 && val[0] != "" {
initiatorID := val[0]
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorID)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID)
}
}
}
return handler(ctx, req)
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds trace information to the request.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
md, ok := metadata.FromIncomingContext(ss.Context())
if ok && md != nil {
if val, ok := md[ctxpkg.TokenHeader]; ok {
if len(val) > 0 && val[0] != "" {
tkn := val[0]
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
}
}
if val, ok := md[ctxpkg.InitiatorHeader]; ok {
if len(val) > 0 && val[0] != "" {
initiatorID := val[0]
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorID)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorID)
}
}
}
wrapped := newWrappedServerStream(ctx, ss)
return handler(srv, wrapped)
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,70 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package useragent
import (
"context"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// NewUnary returns a new unary interceptor that adds
// the useragent to the context.
func NewUnary() grpc.UnaryServerInterceptor {
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 {
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0])
}
}
return handler(ctx, req)
}
return interceptor
}
// NewStream returns a new server stream interceptor
// that adds the user agent to the context.
func NewStream() grpc.StreamServerInterceptor {
interceptor := func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
ctx := ss.Context()
if md, ok := metadata.FromIncomingContext(ctx); ok {
if lst, ok := md[ctxpkg.UserAgentHeader]; ok && len(lst) != 0 {
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, lst[0])
}
}
wrapped := newWrappedServerStream(ctx, ss)
return handler(srv, wrapped)
}
return interceptor
}
func newWrappedServerStream(ctx context.Context, ss grpc.ServerStream) *wrappedServerStream {
return &wrappedServerStream{ServerStream: ss, newCtx: ctx}
}
type wrappedServerStream struct {
grpc.ServerStream
newCtx context.Context
}
func (ss *wrappedServerStream) Context() context.Context {
return ss.newCtx
}
@@ -0,0 +1,162 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package applicationauth
import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appauth"
"github.com/opencloud-eu/reva/v2/pkg/appauth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("applicationauth", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
am appauth.Manager
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
appauthpb.RegisterApplicationsAPIServer(ss, s)
}
func getAppAuthManager(c *config) (appauth.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a app auth provider svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
am, err := getAppAuthManager(c)
if err != nil {
return nil, err
}
service := &service{
conf: c,
am: am,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.auth.applications.v1beta1.ApplicationsAPI/GetAppPassword"}
}
func (s *service) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) {
logger := appctx.GetLogger(ctx)
pwd, err := s.am.GenerateAppPassword(ctx, req.TokenScope, req.Label, req.Expiration)
if err != nil {
logger.Debug().Err(err).Msg("error generating app password")
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewInternal(ctx, "error generating app password"),
}, nil
}
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewOK(ctx),
AppPassword: pwd,
}, nil
}
func (s *service) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) {
pwds, err := s.am.ListAppPasswords(ctx)
if err != nil {
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewInternal(ctx, "error listing app passwords"),
}, nil
}
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewOK(ctx),
AppPasswords: pwds,
}, nil
}
func (s *service) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) {
err := s.am.InvalidateAppPassword(ctx, req.Password)
if err != nil {
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewInternal(ctx, "error invalidating app password"),
}, nil
}
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) {
pwd, err := s.am.GetAppPassword(ctx, req.User, req.Password)
if err != nil {
return &appauthpb.GetAppPasswordResponse{
Status: status.NewStatusFromErrType(ctx, "error getting app password via username/password", err),
}, nil
}
return &appauthpb.GetAppPasswordResponse{
Status: status.NewOK(ctx),
AppPassword: pwd,
}, nil
}
@@ -0,0 +1,208 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appprovider
import (
"context"
"os"
"strconv"
"time"
providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/juliangruber/go-intersect"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/provider/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("appprovider", New)
}
type service struct {
provider app.Provider
conf *config
context context.Context
cancelFunc context.CancelFunc
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
AppProviderURL string `mapstructure:"app_provider_url"`
GatewaySvc string `mapstructure:"gatewaysvc"`
MimeTypes []string `mapstructure:"mime_types"`
Priority uint64 `mapstructure:"priority"`
RefreshTime time.Duration `mapstructure:"refreshtime"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "demo"
}
if c.RefreshTime < 1 {
c.RefreshTime = time.Second * 20
}
c.AppProviderURL = sharedconf.GetGatewaySVC(c.AppProviderURL)
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
c.init()
return c, nil
}
// New creates a new AppProviderService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
provider, err := getProvider(c)
if err != nil {
return nil, err
}
ctx, cancelFunc := context.WithCancel(context.Background())
service := &service{
conf: c,
provider: provider,
context: ctx,
cancelFunc: cancelFunc,
}
t := time.NewTicker(c.RefreshTime)
go func() {
for {
select {
case <-t.C:
service.registerProvider()
case <-service.context.Done():
t.Stop()
}
}
}()
return service, nil
}
func (s *service) registerProvider() {
// Give the appregistry service time to come up
time.Sleep(2 * time.Second)
ctx := context.Background()
log := logger.New().With().Int("pid", os.Getpid()).Logger()
pInfo, err := s.provider.GetAppProviderInfo(ctx)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: could not get provider info")
return
}
pInfo.Address = s.conf.AppProviderURL
if len(s.conf.MimeTypes) != 0 {
mimeTypesIf := intersect.Simple(pInfo.MimeTypes, s.conf.MimeTypes)
var mimeTypes []string
for _, m := range mimeTypesIf {
mimeTypes = append(mimeTypes, m.(string))
}
pInfo.MimeTypes = mimeTypes
}
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: could not get gateway client")
return
}
req := &registrypb.AddAppProviderRequest{Provider: pInfo}
if s.conf.Priority != 0 {
req.Opaque = &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"priority": {
Decoder: "plain",
Value: []byte(strconv.FormatUint(s.conf.Priority, 10)),
},
},
}
}
res, err := client.AddAppProvider(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error registering app provider: error calling add app provider")
return
}
if res.Status.Code != rpc.Code_CODE_OK {
err = status.NewErrorFromCode(res.Status.Code, "appprovider")
log.Error().Err(err).Msg("error registering app provider: add app provider returned error")
return
}
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
providerpb.RegisterProviderAPIServer(ss, s)
}
func getProvider(c *config) (app.Provider, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func (s *service) OpenInApp(ctx context.Context, req *providerpb.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
appURL, err := s.provider.GetAppURL(ctx, req.ResourceInfo, req.ViewMode, req.AccessToken, utils.ReadPlainFromOpaque(req.Opaque, "lang"))
if err != nil {
res := &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, err.Error()),
}
return res, nil
}
res := &providerpb.OpenInAppResponse{
Status: status.NewOK(ctx),
AppUrl: appURL,
}
return res, nil
}
@@ -0,0 +1,196 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package appregistry
import (
"context"
"google.golang.org/grpc"
registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/app"
"github.com/opencloud-eu/reva/v2/pkg/app/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/rs/zerolog"
)
func init() {
rgrpc.Register("appregistry", New)
}
type svc struct {
reg app.Registry
}
func (s *svc) Close() error {
return nil
}
func (s *svc) UnprotectedEndpoints() []string {
return []string{"/cs3.app.registry.v1beta1.RegistryAPI/AddAppProvider", "/cs3.app.registry.v1beta1.RegistryAPI/ListSupportedMimeTypes"}
}
func (s *svc) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new StorageRegistryService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
svc := &svc{
reg: reg,
}
return svc, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
c.init()
return c, nil
}
func getRegistry(c *config) (app.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("appregistrysvc: driver not found: " + c.Driver)
}
func (s *svc) GetAppProviders(ctx context.Context, req *registrypb.GetAppProvidersRequest) (*registrypb.GetAppProvidersResponse, error) {
p, err := s.reg.FindProviders(ctx, req.ResourceInfo.MimeType)
if err != nil {
return &registrypb.GetAppProvidersResponse{
Status: status.NewInternal(ctx, "error looking for the app provider"),
}, nil
}
res := &registrypb.GetAppProvidersResponse{
Status: status.NewOK(ctx),
Providers: p,
}
return res, nil
}
func (s *svc) AddAppProvider(ctx context.Context, req *registrypb.AddAppProviderRequest) (*registrypb.AddAppProviderResponse, error) {
err := s.reg.AddProvider(ctx, req.Provider)
if err != nil {
return &registrypb.AddAppProviderResponse{
Status: status.NewInternal(ctx, "error adding the app provider"),
}, nil
}
res := &registrypb.AddAppProviderResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvidersRequest) (*registrypb.ListAppProvidersResponse, error) {
providers, err := s.reg.ListProviders(ctx)
if err != nil {
return &registrypb.ListAppProvidersResponse{
Status: status.NewInternal(ctx, "error listing the app providers"),
}, nil
}
res := &registrypb.ListAppProvidersResponse{
Status: status.NewOK(ctx),
Providers: providers,
}
return res, nil
}
func (s *svc) ListSupportedMimeTypes(ctx context.Context, req *registrypb.ListSupportedMimeTypesRequest) (*registrypb.ListSupportedMimeTypesResponse, error) {
mimeTypes, err := s.reg.ListSupportedMimeTypes(ctx)
if err != nil {
return &registrypb.ListSupportedMimeTypesResponse{
Status: status.NewInternal(ctx, "error listing the supported mime types"),
}, nil
}
// hide mimetypes for app providers
for _, mime := range mimeTypes {
for _, app := range mime.AppProviders {
app.MimeTypes = nil
}
}
res := &registrypb.ListSupportedMimeTypesResponse{
Status: status.NewOK(ctx),
MimeTypes: mimeTypes,
}
return res, nil
}
func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.GetDefaultAppProviderForMimeTypeRequest) (*registrypb.GetDefaultAppProviderForMimeTypeResponse, error) {
provider, err := s.reg.GetDefaultProviderForMimeType(ctx, req.MimeType)
if err != nil {
return &registrypb.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting the default app provider for the mimetype"),
}, nil
}
res := &registrypb.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewOK(ctx),
Provider: provider,
}
return res, nil
}
func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.SetDefaultAppProviderForMimeTypeRequest) (*registrypb.SetDefaultAppProviderForMimeTypeResponse, error) {
err := s.reg.SetDefaultProviderForMimeType(ctx, req.MimeType, req.Provider)
if err != nil {
return &registrypb.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error setting the default app provider for the mimetype"),
}, nil
}
res := &registrypb.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
@@ -0,0 +1,152 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package authprovider
import (
"context"
"fmt"
"path/filepath"
provider "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("authprovider", New)
}
type config struct {
AuthManager string `mapstructure:"auth_manager"`
AuthManagers map[string]map[string]interface{} `mapstructure:"auth_managers"`
}
func (c *config) init() {
if c.AuthManager == "" {
c.AuthManager = "json"
}
}
type service struct {
authmgr auth.Manager
conf *config
plugin *plugin.RevaPlugin
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.Manager, *plugin.RevaPlugin, error) {
if manager == "" {
return nil, nil, errtypes.InternalError("authsvc: driver not configured for auth manager")
}
p, err := plugin.Load("authprovider", manager)
if err == nil {
authManager, ok := p.Plugin.(auth.Manager)
if !ok {
return nil, nil, fmt.Errorf("could not assert the loaded plugin")
}
pluginConfig := filepath.Base(manager)
err = authManager.Configure(m[pluginConfig])
if err != nil {
return nil, nil, err
}
return authManager, p, nil
} else if _, ok := err.(errtypes.NotFound); ok {
if f, ok := registry.NewFuncs[manager]; ok {
authmgr, err := f(m[manager])
return authmgr, nil, err
}
} else {
return nil, nil, err
}
return nil, nil, errtypes.NotFound(fmt.Sprintf("authsvc: driver %s not found for auth manager", manager))
}
// New returns a new AuthProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
authManager, plug, err := getAuthManager(c.AuthManager, c.AuthManagers)
if err != nil {
return nil, err
}
svc := &service{
conf: c,
authmgr: authManager,
plugin: plug,
}
return svc, nil
}
func (s *service) Close() error {
if s.plugin != nil {
s.plugin.Kill()
}
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.auth.provider.v1beta1.ProviderAPI/Authenticate"}
}
func (s *service) Register(ss *grpc.Server) {
provider.RegisterProviderAPIServer(ss, s)
}
func (s *service) Authenticate(ctx context.Context, req *provider.AuthenticateRequest) (*provider.AuthenticateResponse, error) {
log := appctx.GetLogger(ctx)
username := req.ClientId
password := req.ClientSecret
u, scope, err := s.authmgr.Authenticate(ctx, username, password)
if err != nil {
log.Debug().Str("client_id", username).Err(err).Msg("authsvc: error in Authenticate")
return &provider.AuthenticateResponse{
Status: status.NewStatusFromErrType(ctx, "authsvc: error in Authenticate", err),
}, nil
}
log.Info().Msgf("user %s authenticated", u.Id)
return &provider.AuthenticateResponse{
Status: status.NewOK(ctx),
User: u,
TokenScope: scope,
}, nil
}
@@ -0,0 +1,133 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package authregistry
import (
"context"
registrypb "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/registry/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("authregistry", New)
}
type service struct {
reg auth.Registry
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{
"/cs3.auth.registry.v1beta1.RegistryAPI/GetAuthProvider",
"/cs3.auth.registry.v1beta1.RegistryAPI/ListAuthProviders",
}
}
func (s *service) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new AuthRegistry
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
service := &service{
reg: reg,
}
return service, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func getRegistry(c *config) (auth.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("authregistrysvc: driver not found: " + c.Driver)
}
func (s *service) ListAuthProviders(ctx context.Context, req *registrypb.ListAuthProvidersRequest) (*registrypb.ListAuthProvidersResponse, error) {
pinfos, err := s.reg.ListProviders(ctx)
if err != nil {
return &registrypb.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "error getting list of auth providers"),
}, nil
}
res := &registrypb.ListAuthProvidersResponse{
Status: status.NewOK(ctx),
Providers: pinfos,
}
return res, nil
}
func (s *service) GetAuthProviders(ctx context.Context, req *registrypb.GetAuthProvidersRequest) (*registrypb.GetAuthProvidersResponse, error) {
pinfo, err := s.reg.GetProvider(ctx, req.Type)
if err != nil {
return &registrypb.GetAuthProvidersResponse{
Status: status.NewInternal(ctx, "error getting auth provider for type: "+req.Type),
}, nil
}
res := &registrypb.GetAuthProvidersResponse{
Status: status.NewOK(ctx),
Providers: []*registrypb.ProviderInfo{pinfo},
}
return res, nil
}
@@ -0,0 +1,381 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package datatx
import (
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"sync"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
txdriver "github.com/opencloud-eu/reva/v2/pkg/datatx"
txregistry "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("datatx", New)
}
type config struct {
// transfer driver
TxDriver string `mapstructure:"txdriver"`
TxDrivers map[string]map[string]interface{} `mapstructure:"txdrivers"`
// storage driver to persist share/transfer relation
StorageDriver string `mapstructure:"storage_driver"`
StorageDrivers map[string]map[string]interface{} `mapstructure:"storage_drivers"`
TxSharesFile string `mapstructure:"tx_shares_file"`
DataTransfersFolder string `mapstructure:"data_transfers_folder"`
}
type service struct {
conf *config
txManager txdriver.Manager
txShareDriver *txShareDriver
}
type txShareDriver struct {
sync.Mutex // concurrent access to the file
model *txShareModel
}
type txShareModel struct {
File string
TxShares map[string]*txShare `json:"shares"`
}
type txShare struct {
TxID string
SrcTargetURI string
DestTargetURI string
Opaque *types.Opaque `json:"opaque"`
}
type webdavEndpoint struct {
filePath string
endpoint string
endpointScheme string
token string
}
func (c *config) init() {
if c.TxDriver == "" {
c.TxDriver = "rclone"
}
if c.TxSharesFile == "" {
c.TxSharesFile = "/var/tmp/reva/datatx-shares.json"
}
if c.DataTransfersFolder == "" {
c.DataTransfersFolder = "/home/DataTransfers"
}
}
func (s *service) Register(ss *grpc.Server) {
datatx.RegisterTxAPIServer(ss, s)
}
func getDatatxManager(c *config) (txdriver.Manager, error) {
if f, ok := txregistry.NewFuncs[c.TxDriver]; ok {
return f(c.TxDrivers[c.TxDriver])
}
return nil, errtypes.NotFound("datatx service: driver not found: " + c.TxDriver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "datatx service: error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new datatx svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
txManager, err := getDatatxManager(c)
if err != nil {
return nil, err
}
model, err := loadOrCreate(c.TxSharesFile)
if err != nil {
err = errors.Wrap(err, "datatx service: error loading the file containing the transfer shares")
return nil, err
}
txShareDriver := &txShareDriver{
model: model,
}
service := &service{
conf: c,
txManager: txManager,
txShareDriver: txShareDriver,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) CreateTransfer(ctx context.Context, req *datatx.CreateTransferRequest) (*datatx.CreateTransferResponse, error) {
srcEp, err := s.extractEndpointInfo(ctx, req.SrcTargetUri)
if err != nil {
return nil, err
}
srcRemote := fmt.Sprintf("%s://%s", srcEp.endpointScheme, srcEp.endpoint)
srcPath := srcEp.filePath
srcToken := srcEp.token
destEp, err := s.extractEndpointInfo(ctx, req.DestTargetUri)
if err != nil {
return nil, err
}
dstRemote := fmt.Sprintf("%s://%s", destEp.endpointScheme, destEp.endpoint)
dstPath := destEp.filePath
dstToken := destEp.token
txInfo, startTransferErr := s.txManager.StartTransfer(ctx, srcRemote, srcPath, srcToken, dstRemote, dstPath, dstToken)
// we always save the transfer regardless of start transfer outcome
// only then, if starting fails, can we try to restart it
txShare := &txShare{
TxID: txInfo.GetId().OpaqueId,
SrcTargetURI: req.SrcTargetUri,
DestTargetURI: req.DestTargetUri,
Opaque: req.Opaque,
}
s.txShareDriver.Lock()
defer s.txShareDriver.Unlock()
s.txShareDriver.model.TxShares[txInfo.GetId().OpaqueId] = txShare
if err := s.txShareDriver.model.saveTxShare(); err != nil {
err = errors.Wrap(err, "datatx service: error saving transfer share: "+datatx.Status_STATUS_INVALID.String())
return &datatx.CreateTransferResponse{
Status: status.NewInvalid(ctx, "error pulling transfer"),
}, err
}
// now check start transfer outcome
if startTransferErr != nil {
startTransferErr = errors.Wrap(startTransferErr, "datatx service: error starting transfer job")
return &datatx.CreateTransferResponse{
Status: status.NewInvalid(ctx, "datatx service: error pulling transfer"),
TxInfo: txInfo,
}, startTransferErr
}
return &datatx.CreateTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, err
}
func (s *service) GetTransferStatus(ctx context.Context, req *datatx.GetTransferStatusRequest) (*datatx.GetTransferStatusResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.GetTransferStatus(ctx, req.GetTxId().OpaqueId)
if err != nil {
return &datatx.GetTransferStatusResponse{
Status: status.NewInternal(ctx, "datatx service: error getting transfer status"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.GetTransferStatusResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) CancelTransfer(ctx context.Context, req *datatx.CancelTransferRequest) (*datatx.CancelTransferResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.CancelTransfer(ctx, req.GetTxId().OpaqueId)
if err != nil {
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.CancelTransferResponse{
Status: status.NewInternal(ctx, "error cancelling transfer"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.CancelTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) ListTransfers(ctx context.Context, req *datatx.ListTransfersRequest) (*datatx.ListTransfersResponse, error) {
filters := req.Filters
var txInfos []*datatx.TxInfo
for _, txShare := range s.txShareDriver.model.TxShares {
if len(filters) == 0 {
txInfos = append(txInfos, &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txShare.TxID},
ShareId: &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)},
})
} else {
for _, f := range filters {
if f.Type == datatx.ListTransfersRequest_Filter_TYPE_SHARE_ID {
if f.GetShareId().GetOpaqueId() == string(txShare.Opaque.Map["shareId"].Value) {
txInfos = append(txInfos, &datatx.TxInfo{
Id: &datatx.TxId{OpaqueId: txShare.TxID},
ShareId: &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)},
})
}
}
}
}
}
return &datatx.ListTransfersResponse{
Status: status.NewOK(ctx),
Transfers: txInfos,
}, nil
}
func (s *service) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRequest) (*datatx.RetryTransferResponse, error) {
txShare, ok := s.txShareDriver.model.TxShares[req.GetTxId().GetOpaqueId()]
if !ok {
return nil, errtypes.InternalError("datatx service: transfer not found")
}
txInfo, err := s.txManager.RetryTransfer(ctx, req.GetTxId().OpaqueId)
if err != nil {
return &datatx.RetryTransferResponse{
Status: status.NewInternal(ctx, "error retrying transfer"),
TxInfo: txInfo,
}, err
}
txInfo.ShareId = &ocm.ShareId{OpaqueId: string(txShare.Opaque.Map["shareId"].Value)}
return &datatx.RetryTransferResponse{
Status: status.NewOK(ctx),
TxInfo: txInfo,
}, nil
}
func (s *service) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) {
if targetURL == "" {
return nil, errtypes.BadRequest("datatx service: ref target is an empty uri")
}
uri, err := url.Parse(targetURL)
if err != nil {
return nil, errors.Wrap(err, "datatx service: error parsing target uri: "+targetURL)
}
m, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return nil, errors.Wrap(err, "datatx service: error parsing target resource name")
}
return &webdavEndpoint{
filePath: m["name"][0],
endpoint: uri.Host + uri.Path,
endpointScheme: uri.Scheme,
token: uri.User.String(),
}, nil
}
func loadOrCreate(file string) (*txShareModel, error) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
err = errors.Wrap(err, "datatx service: error creating the transfer shares storage file: "+file)
return nil, err
}
}
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
if err != nil {
err = errors.Wrap(err, "datatx service: error opening the transfer shares storage file: "+file)
return nil, err
}
defer fd.Close()
data, err := io.ReadAll(fd)
if err != nil {
err = errors.Wrap(err, "datatx service: error reading the data")
return nil, err
}
model := &txShareModel{}
if err := json.Unmarshal(data, model); err != nil {
err = errors.Wrap(err, "datatx service: error decoding transfer shares data to json")
return nil, err
}
if model.TxShares == nil {
model.TxShares = make(map[string]*txShare)
}
model.File = file
return model, nil
}
func (m *txShareModel) saveTxShare() error {
data, err := json.Marshal(m)
if err != nil {
err = errors.Wrap(err, "datatx service: error encoding transfer share data to json")
return err
}
if err := os.WriteFile(m.File, data, 0644); err != nil {
err = errors.Wrap(err, "datatx service: error writing transfer share data to file: "+m.File)
return err
}
return nil
}
@@ -0,0 +1,92 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
appauthpb "github.com/cs3org/go-cs3apis/cs3/auth/applications/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GenerateAppPassword(ctx context.Context, req *appauthpb.GenerateAppPasswordRequest) (*appauthpb.GenerateAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.GenerateAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.GenerateAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GenerateAppPassword")
}
return res, nil
}
func (s *svc) ListAppPasswords(ctx context.Context, req *appauthpb.ListAppPasswordsRequest) (*appauthpb.ListAppPasswordsResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.ListAppPasswordsResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.ListAppPasswords(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAppPasswords")
}
return res, nil
}
func (s *svc) InvalidateAppPassword(ctx context.Context, req *appauthpb.InvalidateAppPasswordRequest) (*appauthpb.InvalidateAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.InvalidateAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.InvalidateAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling InvalidateAppPassword")
}
return res, nil
}
func (s *svc) GetAppPassword(ctx context.Context, req *appauthpb.GetAppPasswordRequest) (*appauthpb.GetAppPasswordResponse, error) {
c, err := pool.GetAppAuthProviderServiceClient(s.c.ApplicationAuthEndpoint)
if err != nil {
return &appauthpb.GetAppPasswordResponse{
Status: status.NewInternal(ctx, "error getting app auth provider client"),
}, nil
}
res, err := c.GetAppPassword(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppPassword")
}
return res, nil
}
@@ -0,0 +1,340 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"crypto/tls"
"fmt"
"net/url"
"strings"
providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
providerv1beta1 "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"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
func (s *svc) OpenInApp(ctx context.Context, req *gateway.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) {
statRes, err := s.Stat(ctx, &storageprovider.StatRequest{
Ref: req.Ref,
})
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
if statRes.Status.Code != rpc.Code_CODE_OK {
return &providerpb.OpenInAppResponse{
Status: statRes.Status,
}, nil
}
fileInfo := statRes.Info
// The file is a share
if fileInfo.Type == storageprovider.ResourceType_RESOURCE_TYPE_REFERENCE {
uri, err := url.Parse(fileInfo.Target)
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error parsing target uri: "+fileInfo.Target),
}, nil
}
if uri.Scheme == "webdav" {
insecure, skipVerify := getGRPCConfig(req.Opaque)
return s.openFederatedShares(ctx, fileInfo.Target, req.ViewMode, req.App, insecure, skipVerify, "")
}
res, err := s.Stat(ctx, &storageprovider.StatRequest{
Ref: req.Ref,
})
if err != nil {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "gateway: error calling Stat on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
if res.Status.Code != rpc.Code_CODE_OK {
return &providerpb.OpenInAppResponse{
Status: status.NewInternal(ctx, "Stat failed on the resource path for the app provider: "+req.Ref.GetPath()),
}, nil
}
fileInfo = res.Info
}
return s.openLocalResources(ctx, fileInfo, req.ViewMode, req.App, req.Opaque)
}
func (s *svc) openFederatedShares(ctx context.Context, targetURL string, vm gateway.OpenInAppRequest_ViewMode, app string,
insecure, skipVerify bool, nameQueries ...string) (*providerpb.OpenInAppResponse, error) {
log := appctx.GetLogger(ctx)
targetURL, err := appendNameQuery(targetURL, nameQueries...)
if err != nil {
return nil, err
}
ep, err := s.extractEndpointInfo(ctx, targetURL)
if err != nil {
return nil, err
}
ref := &storageprovider.Reference{Path: ep.filePath}
appProviderReq := &gateway.OpenInAppRequest{
Ref: ref,
ViewMode: vm,
App: app,
}
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: ep.endpoint,
})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
var gatewayEP string
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "gateway" {
gatewayEP = s.Endpoint.Path
}
}
log.Debug().Msgf("Forwarding OpenInApp request to: %s", gatewayEP)
conn, err := getConn(gatewayEP, insecure, skipVerify)
if err != nil {
log.Err(err).Msg("error connecting to remote reva")
return nil, errors.Wrap(err, "gateway: error connecting to remote reva")
}
gatewayClient := gateway.NewGatewayAPIClient(conn)
remoteCtx := ctxpkg.ContextSetToken(context.Background(), ep.token)
remoteCtx = metadata.AppendToOutgoingContext(remoteCtx, ctxpkg.TokenHeader, ep.token)
res, err := gatewayClient.OpenInApp(remoteCtx, appProviderReq)
if err != nil {
log.Err(err).Msg("error returned by remote OpenInApp call")
return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
}
return res, nil
}
func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.ResourceInfo,
vm gateway.OpenInAppRequest_ViewMode, app string, opaque *typespb.Opaque) (*providerpb.OpenInAppResponse, error) {
accessToken, ok := ctxpkg.ContextGetToken(ctx)
if !ok || accessToken == "" {
return &providerpb.OpenInAppResponse{
Status: status.NewUnauthenticated(ctx, errtypes.InvalidCredentials("Access token is invalid or empty"), ""),
}, nil
}
provider, err := s.findAppProvider(ctx, ri, app)
if err != nil {
err = errors.Wrap(err, "gateway: error calling findAppProvider")
if _, ok := err.(errtypes.IsNotFound); ok {
return &providerpb.OpenInAppResponse{
Status: status.NewNotFound(ctx, "Could not find the requested app provider"),
}, nil
}
return nil, err
}
appProviderClient, err := pool.GetAppProviderClient(provider.Address)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppProviderClient")
}
appProviderReq, err := buildOpenInAppRequest(ctx, ri, vm, s.tokenmgr, accessToken, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error building OpenInApp request")
}
res, err := appProviderClient.OpenInApp(ctx, appProviderReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling OpenInApp")
}
return res, nil
}
func buildOpenInAppRequest(ctx context.Context, ri *storageprovider.ResourceInfo, vm gateway.OpenInAppRequest_ViewMode, tokenmgr token.Manager, accessToken string, opaque *typespb.Opaque) (*providerpb.OpenInAppRequest, error) {
// in case of a view only mode and a stat permission we need to create a view only token
if vm == gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY && ri.GetPermissionSet().GetStat() {
// Limit scope to the resource
scope, err := scope.AddResourceInfoScope(ri, providerv1beta1.Role_ROLE_VIEWER, nil)
if err != nil {
return nil, err
}
// build a fake user object for the token
currentuser, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
scopedUser := &userpb.User{
Id: ri.GetOwner(), // the owner of the resource is always set, right?
DisplayName: "View Only user for " + currentuser.GetUsername(),
}
// mint a view only token
viewOnlyToken, err := tokenmgr.MintToken(ctx, scopedUser, scope)
if err != nil {
return nil, err
}
// TODO we should not append the token to the opaque, we should have a dedicated field in the request
opaque = utils.AppendPlainToOpaque(opaque, "viewOnlyToken", viewOnlyToken)
}
return &providerpb.OpenInAppRequest{
ResourceInfo: ri,
ViewMode: providerpb.ViewMode(vm),
AccessToken: accessToken,
// ViewOnlyToken: viewOnlyToken // scoped to the shared resource if the stat response hase a ViewOnly permission
Opaque: opaque,
}, nil
}
func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo, app string) (*registry.ProviderInfo, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
err = errors.Wrap(err, "gateway: error getting appregistry client")
return nil, err
}
// when app is empty it means the user assumes a default behaviour.
// From a web perspective, means the user click on the file itself.
// Normally the file will get downloaded but if a suitable application exists
// the behaviour will change from download to open the file with the app.
if app == "" {
// If app is empty means that we need to rely on "default" behaviour.
// We currently do not have user preferences implemented so the only default
// we can currently enforce is one configured by the system admins, the
// "system default".
// If a default is not set we raise an error rather that giving the user the first provider in the list
// as the list is built on init time and is not deterministic, giving the user different results on service
// reload.
res, err := c.GetDefaultAppProviderForMimeType(ctx, &registry.GetDefaultAppProviderForMimeTypeRequest{
MimeType: ri.MimeType,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
return nil, err
}
// we've found a provider
if res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil {
return res.Provider, nil
}
// we did not find a default provider
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
err := errtypes.NotFound(fmt.Sprintf("gateway: default app provider for mime type:%s not found", ri.MimeType))
return nil, err
}
// response code is something else, bubble up error
// if a default is not set we abort as returning the first application available is not
// deterministic for the end-user as it depends on initialization order of the app approviders with the registry.
// It also provides a good hint to the system admin to configure the defaults accordingly.
err = errtypes.InternalError(fmt.Sprintf("gateway: unexpected grpc response status:%s when calling GetDefaultAppProviderForMimeType", res.Status))
return nil, err
}
// app has been forced and is set, we try to get an app provider that can satisfy it
// Note that we ask for the list of all available providers for a given resource
// even though we're only interested into the one set by the "app" parameter.
// A better call will be to issue a (to be added) GetAppProviderByName(app) method
// to just get what we ask for.
res, err := c.GetAppProviders(ctx, &registry.GetAppProvidersRequest{
ResourceInfo: ri,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAppProviders")
return nil, err
}
// if the list of app providers is empty means we expect a CODE_NOT_FOUND in the response
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String())
}
return nil, errtypes.InternalError("gateway: error finding app providers")
}
// as long as the above mentioned GetAppProviderByName(app) method is not available
// we need to apply a manual filter
filteredProviders := []*registry.ProviderInfo{}
for _, p := range res.Providers {
if p.Name == app {
filteredProviders = append(filteredProviders, p)
}
}
res.Providers = filteredProviders
if len(res.Providers) == 0 {
return nil, errtypes.NotFound(fmt.Sprintf("app '%s' not found", app))
}
if len(res.Providers) == 1 {
return res.Providers[0], nil
}
// we should never arrive to the point of having more than one
// provider for the given "app" parameters sent by the user
return nil, errtypes.InternalError(fmt.Sprintf("gateway: user requested app %q and we provided %d applications", app, len(res.Providers)))
}
func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) {
if opaque == nil {
return false, false
}
_, insecure := opaque.Map["insecure"]
_, skipVerify := opaque.Map["skip-verify"]
return insecure, skipVerify
}
func getConn(host string, ins, skipverify bool) (*grpc.ClientConn, error) {
if ins {
return grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
// TODO(labkode): if in the future we want client-side certificate validation,
// we need to load the client cert here
tlsconf := &tls.Config{InsecureSkipVerify: skipverify}
creds := credentials.NewTLS(tlsconf)
return grpc.NewClient(host, grpc.WithTransportCredentials(creds))
}
@@ -0,0 +1,124 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetAppProviders(ctx context.Context, req *registry.GetAppProvidersRequest) (*registry.GetAppProvidersResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.GetAppProvidersResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.GetAppProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAppProviders")
}
return res, nil
}
func (s *svc) AddAppProvider(ctx context.Context, req *registry.AddAppProviderRequest) (*registry.AddAppProviderResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.AddAppProviderResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.AddAppProvider(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AddAppProvider")
}
return res, nil
}
func (s *svc) ListAppProviders(ctx context.Context, req *registry.ListAppProvidersRequest) (*registry.ListAppProvidersResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.ListAppProvidersResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.ListAppProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAppProviders")
}
return res, nil
}
func (s *svc) ListSupportedMimeTypes(ctx context.Context, req *registry.ListSupportedMimeTypesRequest) (*registry.ListSupportedMimeTypesResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.ListSupportedMimeTypesResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.ListSupportedMimeTypes(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListSupportedMimeTypes")
}
return res, nil
}
func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registry.GetDefaultAppProviderForMimeTypeRequest) (*registry.GetDefaultAppProviderForMimeTypeResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.GetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.GetDefaultAppProviderForMimeType(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType")
}
return res, nil
}
func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registry.SetDefaultAppProviderForMimeTypeRequest) (*registry.SetDefaultAppProviderForMimeTypeResponse, error) {
c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint)
if err != nil {
return &registry.SetDefaultAppProviderForMimeTypeResponse{
Status: status.NewInternal(ctx, "error getting app registry client"),
}, nil
}
res, err := c.SetDefaultAppProviderForMimeType(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling SetDefaultAppProviderForMimeType")
}
return res, nil
}
@@ -0,0 +1,206 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
"fmt"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)
func (s *svc) Authenticate(ctx context.Context, req *gateway.AuthenticateRequest) (*gateway.AuthenticateResponse, error) {
log := appctx.GetLogger(ctx)
// find auth provider
c, err := s.findAuthProvider(ctx, req.Type)
if err != nil {
log.Err(err).Str("type", req.Type).Msg("error getting auth provider client")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error getting auth provider client"),
}, nil
}
authProviderReq := &authpb.AuthenticateRequest{
ClientId: req.ClientId,
ClientSecret: req.ClientSecret,
}
res, err := c.Authenticate(ctx, authProviderReq)
switch {
case err != nil:
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, fmt.Sprintf("gateway: error calling Authenticate for type: %s", req.Type)),
}, nil
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:
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, fmt.Sprintf("error authenticating credentials to auth provider for type: %s", req.Type)),
}, nil
}
// validate valid userId
if res.User == nil {
err := errtypes.NotFound("gateway: user after Authenticate is nil")
log.Err(err).Msg("user is nil")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "user is nil"),
}, nil
}
if res.User.Id == nil {
err := errtypes.NotFound("gateway: uid after Authenticate is nil")
log.Err(err).Msg("user id is nil")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "user id is nil"),
}, nil
}
u := proto.Clone(res.User).(*userpb.User)
if sharedconf.SkipUserGroupsInToken() {
u.Groups = []string{}
}
token, err := s.tokenmgr.MintToken(ctx, u, res.TokenScope)
if err != nil {
err = errors.Wrap(err, "authsvc: error in MintToken")
res := &gateway.AuthenticateResponse{
Status: status.NewUnauthenticated(ctx, err, "error creating access token"),
}
return res, nil
}
if scope, ok := res.TokenScope["user"]; s.c.DisableHomeCreationOnLogin || !ok || scope.Role != authpb.Role_ROLE_OWNER || res.User.Id.Type == userpb.UserType_USER_TYPE_FEDERATED {
gwRes := &gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
User: res.User,
Token: token,
}
return gwRes, nil
}
// we need to pass the token to authenticate the CreateHome request.
// TODO(labkode): appending to existing context will not pass the token.
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = ctxpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials?
// create home directory
createHomeRes, err := s.CreateHome(ctx, &storageprovider.CreateHomeRequest{})
if err != nil {
log.Err(err).Msg("error calling CreateHome")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error creating user home"),
}, nil
}
if createHomeRes.Status.Code != rpc.Code_CODE_OK && createHomeRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
err := status.NewErrorFromCode(createHomeRes.Status.Code, "gateway")
log.Err(err).Msg("error calling Createhome")
return &gateway.AuthenticateResponse{
Status: status.NewInternal(ctx, "error creating user home"),
}, nil
}
gwRes := &gateway.AuthenticateResponse{
Status: status.NewOK(ctx),
User: res.User,
Token: token,
}
return gwRes, nil
}
func (s *svc) WhoAmI(ctx context.Context, req *gateway.WhoAmIRequest) (*gateway.WhoAmIResponse, error) {
u, _, err := s.tokenmgr.DismantleToken(ctx, req.Token)
if err != nil {
err = errors.Wrap(err, "gateway: error getting user from token")
return &gateway.WhoAmIResponse{
Status: status.NewUnauthenticated(ctx, err, "error dismantling token"),
}, nil
}
if sharedconf.SkipUserGroupsInToken() {
groupsRes, err := s.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
return nil, err
}
u.Groups = groupsRes.Groups
}
res := &gateway.WhoAmIResponse{
Status: status.NewOK(ctx),
User: u,
}
return res, nil
}
func (s *svc) findAuthProvider(ctx context.Context, authType string) (authpb.ProviderAPIClient, error) {
c, err := pool.GetAuthRegistryServiceClient(s.c.AuthRegistryEndpoint)
if err != nil {
err = errors.Wrap(err, "gateway: error getting auth registry client")
return nil, err
}
res, err := c.GetAuthProviders(ctx, &registry.GetAuthProvidersRequest{
Type: authType,
})
if err != nil {
err = errors.Wrap(err, "gateway: error calling GetAuthProvider")
return nil, err
}
if res.Status.Code == rpc.Code_CODE_OK && res.Providers != nil && len(res.Providers) > 0 {
// TODO(labkode): check for capabilities here
c, err := pool.GetAuthProviderServiceClient(res.Providers[0].Address)
if err != nil {
err = errors.Wrap(err, "gateway: error getting an auth provider client")
return nil, err
}
return c, nil
}
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound("gateway: auth provider not found for type:" + authType)
}
return nil, errtypes.InternalError("gateway: error finding an auth provider for type: " + authType)
}
@@ -0,0 +1,64 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
registry "github.com/cs3org/go-cs3apis/cs3/auth/registry/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
func (s *svc) ListAuthProviders(ctx context.Context, req *registry.ListAuthProvidersRequest) (*gateway.ListAuthProvidersResponse, error) {
c, err := pool.GetAuthRegistryServiceClient(s.c.AuthRegistryEndpoint)
if err != nil {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
res, err := c.ListAuthProviders(ctx, req)
if err != nil {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
if res.Status.Code != rpc.Code_CODE_OK {
return &gateway.ListAuthProvidersResponse{
Status: status.NewInternal(ctx, "gateway"),
}, nil
}
types := make([]string, len(res.Providers))
for i, p := range res.Providers {
types[i] = p.ProviderType
}
gwRes := &gateway.ListAuthProvidersResponse{
Status: res.Status,
Opaque: res.Opaque,
Types: types,
}
return gwRes, nil
}
@@ -0,0 +1,108 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreateTransfer(ctx context.Context, req *datatx.CreateTransferRequest) (*datatx.CreateTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.CreateTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.CreateTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling PullTransfer")
}
return res, nil
}
func (s *svc) GetTransferStatus(ctx context.Context, req *datatx.GetTransferStatusRequest) (*datatx.GetTransferStatusResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.GetTransferStatusResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.GetTransferStatus(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTransferStatus")
}
return res, nil
}
func (s *svc) CancelTransfer(ctx context.Context, req *datatx.CancelTransferRequest) (*datatx.CancelTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.CancelTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.CancelTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CancelTransfer")
}
return res, nil
}
func (s *svc) ListTransfers(ctx context.Context, req *datatx.ListTransfersRequest) (*datatx.ListTransfersResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.ListTransfersResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.ListTransfers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListTransfers")
}
return res, nil
}
func (s *svc) RetryTransfer(ctx context.Context, req *datatx.RetryTransferRequest) (*datatx.RetryTransferResponse, error) {
c, err := pool.GetDataTxClient(s.c.DataTxEndpoint)
if err != nil {
return &datatx.RetryTransferResponse{
Status: status.NewInternal(ctx, "error getting data transfer client"),
}, nil
}
res, err := c.RetryTransfer(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RetryTransfer")
}
return res, nil
}
@@ -0,0 +1,213 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"fmt"
"net/url"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
"github.com/opencloud-eu/reva/v2/pkg/token"
"github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("gateway", New)
}
type config struct {
AuthRegistryEndpoint string `mapstructure:"authregistrysvc"`
ApplicationAuthEndpoint string `mapstructure:"applicationauthsvc"`
StorageRegistryEndpoint string `mapstructure:"storageregistrysvc"`
AppRegistryEndpoint string `mapstructure:"appregistrysvc"`
PreferencesEndpoint string `mapstructure:"preferencessvc"`
UserShareProviderEndpoint string `mapstructure:"usershareprovidersvc"`
PublicShareProviderEndpoint string `mapstructure:"publicshareprovidersvc"`
OCMShareProviderEndpoint string `mapstructure:"ocmshareprovidersvc"`
OCMInviteManagerEndpoint string `mapstructure:"ocminvitemanagersvc"`
OCMProviderAuthorizerEndpoint string `mapstructure:"ocmproviderauthorizersvc"`
OCMCoreEndpoint string `mapstructure:"ocmcoresvc"`
UserProviderEndpoint string `mapstructure:"userprovidersvc"`
GroupProviderEndpoint string `mapstructure:"groupprovidersvc"`
TenantProviderEndpoint string `mapstructure:"tenantprovidersvc"`
DataTxEndpoint string `mapstructure:"datatx"`
DataGatewayEndpoint string `mapstructure:"datagateway"`
PermissionsEndpoint string `mapstructure:"permissionssvc"`
CommitShareToStorageGrant bool `mapstructure:"commit_share_to_storage_grant"`
DisableHomeCreationOnLogin bool `mapstructure:"disable_home_creation_on_login"`
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
TransferExpires int64 `mapstructure:"transfer_expires"`
TokenManager string `mapstructure:"token_manager"`
// ShareFolder is the location where to create shares in the recipient's storage provider.
// FIXME get rid of ShareFolder, there are findByPath calls in the ocmshareporvider.go and usershareprovider.go
ShareFolder string `mapstructure:"share_folder"`
DataTransfersFolder string `mapstructure:"data_transfers_folder"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
AllowedUserAgents map[string][]string `mapstructure:"allowed_user_agents"` // map[path][]user-agent
CreatePersonalSpaceCacheConfig cache.Config `mapstructure:"create_personal_space_cache_config"`
ProviderCacheConfig cache.Config `mapstructure:"provider_cache_config"`
}
// sets defaults
func (c *config) init() {
if c.ShareFolder == "" {
c.ShareFolder = "MyShares"
}
c.ShareFolder = strings.Trim(c.ShareFolder, "/")
if c.DataTransfersFolder == "" {
c.DataTransfersFolder = "DataTransfers"
}
if c.TokenManager == "" {
c.TokenManager = "jwt"
}
// if services address are not specified we used the shared conf
// for the gatewaysvc to have dev setups very quickly.
c.AuthRegistryEndpoint = sharedconf.GetGatewaySVC(c.AuthRegistryEndpoint)
c.ApplicationAuthEndpoint = sharedconf.GetGatewaySVC(c.ApplicationAuthEndpoint)
c.StorageRegistryEndpoint = sharedconf.GetGatewaySVC(c.StorageRegistryEndpoint)
c.AppRegistryEndpoint = sharedconf.GetGatewaySVC(c.AppRegistryEndpoint)
c.PreferencesEndpoint = sharedconf.GetGatewaySVC(c.PreferencesEndpoint)
c.UserShareProviderEndpoint = sharedconf.GetGatewaySVC(c.UserShareProviderEndpoint)
c.PublicShareProviderEndpoint = sharedconf.GetGatewaySVC(c.PublicShareProviderEndpoint)
c.OCMShareProviderEndpoint = sharedconf.GetGatewaySVC(c.OCMShareProviderEndpoint)
c.OCMInviteManagerEndpoint = sharedconf.GetGatewaySVC(c.OCMInviteManagerEndpoint)
c.OCMProviderAuthorizerEndpoint = sharedconf.GetGatewaySVC(c.OCMProviderAuthorizerEndpoint)
c.OCMCoreEndpoint = sharedconf.GetGatewaySVC(c.OCMCoreEndpoint)
c.UserProviderEndpoint = sharedconf.GetGatewaySVC(c.UserProviderEndpoint)
c.GroupProviderEndpoint = sharedconf.GetGatewaySVC(c.GroupProviderEndpoint)
// Fall back to userprovidersvc when no dedicated tenant provider is configured.
if c.TenantProviderEndpoint == "" {
c.TenantProviderEndpoint = c.UserProviderEndpoint
} else {
c.TenantProviderEndpoint = sharedconf.GetGatewaySVC(c.TenantProviderEndpoint)
}
c.DataTxEndpoint = sharedconf.GetGatewaySVC(c.DataTxEndpoint)
c.DataGatewayEndpoint = sharedconf.GetDataGateway(c.DataGatewayEndpoint)
// use shared secret if not set
c.TransferSharedSecret = sharedconf.GetJWTSecret(c.TransferSharedSecret)
// lifetime for the transfer token (TUS upload)
if c.TransferExpires == 0 {
c.TransferExpires = 100 * 60 // seconds
}
// caching needs to be explicitly enabled
if c.ProviderCacheConfig.Store == "" {
c.ProviderCacheConfig.Store = "noop"
}
if c.ProviderCacheConfig.Database == "" {
c.ProviderCacheConfig.Database = "reva"
}
if c.CreatePersonalSpaceCacheConfig.Store == "" {
c.CreatePersonalSpaceCacheConfig.Store = "memory"
}
if c.CreatePersonalSpaceCacheConfig.Database == "" {
c.CreatePersonalSpaceCacheConfig.Database = "reva"
}
}
type svc struct {
c *config
dataGatewayURL url.URL
tokenmgr token.Manager
providerCache cache.ProviderCache
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// New creates a new gateway svc that acts as a proxy for any grpc operation.
// The gateway is responsible for high-level controls: rate-limiting, coordination between svcs
// like sharing and storage acls, asynchronous transactions, ...
func New(m map[string]interface{}, _ *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
// ensure DataGatewayEndpoint is a valid URI
u, err := url.Parse(c.DataGatewayEndpoint)
if err != nil {
return nil, err
}
tokenManager, err := getTokenManager(c.TokenManager, c.TokenManagers)
if err != nil {
return nil, err
}
s := &svc{
c: c,
dataGatewayURL: *u,
tokenmgr: tokenManager,
providerCache: cache.GetProviderCache(c.ProviderCacheConfig),
createPersonalSpaceCache: cache.GetCreatePersonalSpaceCache(c.CreatePersonalSpaceCacheConfig),
}
return s, nil
}
func (s *svc) Register(ss *grpc.Server) {
gateway.RegisterGatewayAPIServer(ss, s)
}
func (s *svc) Close() error {
s.providerCache.Close()
s.createPersonalSpaceCache.Close()
return nil
}
func (s *svc) UnprotectedEndpoints() []string {
return []string{"/cs3.gateway.v1beta1.GatewayAPI"}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "gateway: error decoding conf")
return nil, err
}
return c, nil
}
func getTokenManager(manager string, m map[string]map[string]interface{}) (token.Manager, error) {
if f, ok := registry.NewFuncs[manager]; ok {
return f(m[manager])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for token manager", manager))
}
@@ -0,0 +1,108 @@
// 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 gateway
import (
"context"
group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetGroup(ctx context.Context, req *group.GetGroupRequest) (*group.GetGroupResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetGroupResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetGroup(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetGroup")
}
return res, nil
}
func (s *svc) GetGroupByClaim(ctx context.Context, req *group.GetGroupByClaimRequest) (*group.GetGroupByClaimResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetGroupByClaimResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetGroupByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetGroupByClaim")
}
return res, nil
}
func (s *svc) FindGroups(ctx context.Context, req *group.FindGroupsRequest) (*group.FindGroupsResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.FindGroupsResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.FindGroups(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindGroups")
}
return res, nil
}
func (s *svc) GetMembers(ctx context.Context, req *group.GetMembersRequest) (*group.GetMembersResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.GetMembersResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetMembers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetMembers")
}
return res, nil
}
func (s *svc) HasMember(ctx context.Context, req *group.HasMemberRequest) (*group.HasMemberResponse, error) {
c, err := pool.GetGroupProviderServiceClient(s.c.GroupProviderEndpoint)
if err != nil {
return &group.HasMemberResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.HasMember(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling HasMember")
}
return res, nil
}
@@ -0,0 +1,76 @@
// 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 gateway
import (
"context"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.CreateOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CreateOCMCoreShare")
}
return res, nil
}
func (s *svc) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.UpdateOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.UpdateOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint)
if err != nil {
return &ocmcore.DeleteOCMCoreShareResponse{
Status: status.NewInternal(ctx, "error getting ocm core client"),
}, nil
}
res, err := c.DeleteOCMCoreShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare")
}
return res, nil
}
@@ -0,0 +1,140 @@
// 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 gateway
import (
"context"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.GenerateInviteToken(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GenerateInviteToken")
}
return res, nil
}
func (s *svc) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.ListInviteTokens(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListInviteTokens")
}
return res, nil
}
func (s *svc) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.ForwardInvite(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ForwardInvite")
}
return res, nil
}
func (s *svc) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.AcceptInvite(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AcceptInvite")
}
return res, nil
}
func (s *svc) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.GetAcceptedUserResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.GetAcceptedUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetAcceptedUser")
}
return res, nil
}
func (s *svc) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.FindAcceptedUsers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers")
}
return res, nil
}
func (s *svc) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint)
if err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error getting user invite provider client"),
}, nil
}
res, err := c.DeleteAcceptedUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers")
}
return res, nil
}
@@ -0,0 +1,76 @@
// 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 gateway
import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) IsProviderAllowed(ctx context.Context, req *ocmprovider.IsProviderAllowedRequest) (*ocmprovider.IsProviderAllowedResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.IsProviderAllowed(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling IsProviderAllowed")
}
return res, nil
}
func (s *svc) GetInfoByDomain(ctx context.Context, req *ocmprovider.GetInfoByDomainRequest) (*ocmprovider.GetInfoByDomainResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.GetInfoByDomain(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
return res, nil
}
func (s *svc) ListAllProviders(ctx context.Context, req *ocmprovider.ListAllProvidersRequest) (*ocmprovider.ListAllProvidersResponse, error) {
c, err := pool.GetOCMProviderAuthorizerClient(s.c.OCMProviderAuthorizerEndpoint)
if err != nil {
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewInternal(ctx, "error getting ocm authorizer provider client"),
}, nil
}
res, err := c.ListAllProviders(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListAllProviders")
}
return res, nil
}
@@ -0,0 +1,465 @@
// 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 gateway
import (
"context"
"fmt"
"net/url"
"path"
"slices"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
incoming "github.com/cs3org/go-cs3apis/cs3/ocm/incoming/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
// TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled.
func (s *svc) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) {
if len(req.AccessMethods) == 0 {
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, "access methods cannot be empty"),
}, nil
}
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
// persist the OCM share in the ocm share provider
res, err := c.CreateOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling CreateOCMShare")
}
// add a grant to the storage provider so the share can efficiently be listed
// the grant does not grant any permissions. access is granted by the OCM link token
// that is used by the public storage provider to impersonate the resource owner
status, err := s.addGrant(ctx, req.ResourceId, req.Grantee, req.AccessMethods[0].GetWebdavOptions().Permissions, req.Expiration, nil)
switch {
case err != nil:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
case status.Code == rpc.Code_CODE_UNIMPLEMENTED:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
case status.Code != rpc.Code_CODE_OK:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful")
return &ocm.CreateOCMShareResponse{
Status: status,
}, nil
}
return res, nil
}
func (s *svc) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
getShareRes, err := c.GetOCMShare(ctx, &ocm.GetOCMShareRequest{
Ref: req.Ref,
})
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShare")
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx,
"error getting ocm share when committing to the storage"),
}
return res, nil
}
share := getShareRes.Share
res, err := c.RemoveOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveOCMShare")
}
// remove the grant from the storage provider
status, err := s.removeGrant(ctx, share.GetResourceId(), share.GetGrantee(), share.GetAccessMethods()[0].GetWebdavOptions().GetPermissions(), nil)
if err != nil {
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
}
if status.Code != rpc.Code_CODE_OK {
return &ocm.RemoveOCMShareResponse{
Status: status,
}, err
}
return res, nil
}
// TODO(labkode): we need to validate share state vs storage grant and storage ref
// If there are any inconsistencies, the share needs to be flag as invalid and a background process
// or active fix needs to be performed.
func (s *svc) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
return s.getOCMShare(ctx, req)
}
func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.GetOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetOCMShare")
}
return res, nil
}
func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) {
c, err := pool.GetOCMShareProviderClient(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(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.ListOCMShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListOCMShares")
}
return res, nil
}
func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.UpdateOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMShare")
}
return res, nil
}
func (s *svc) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.ListReceivedOCMShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListReceivedOCMShares")
}
return res, nil
}
func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) {
log := appctx.GetLogger(ctx)
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
// retrieve the current received share
getShareReq := &ocm.GetReceivedOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: req.Share.Id,
},
},
}
getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq)
if err != nil {
log.Error().Err(err).Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Msg("gateway: error calling GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: error calling GetReceivedOCMShare",
},
}, nil
}
share := getShareRes.Share
if share == nil {
log.Error().Err(err).Msg("gateway: got a nil share from GetReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
Message: "gateway: got a nil share from GetReceivedOCMShare",
},
}, nil
}
res, err := c.UpdateReceivedOCMShare(ctx, req)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
for i := range req.UpdateMask.Paths {
switch req.UpdateMask.Paths[i] {
case "state":
switch req.GetShare().GetState() {
case ocm.ShareState_SHARE_STATE_ACCEPTED:
// for a transfer this is handled elsewhere
case ocm.ShareState_SHARE_STATE_PENDING:
// currently no consequences
case ocm.ShareState_SHARE_STATE_REJECTED:
// TODO
return res, nil
}
case "mount_point":
// TODO(labkode): implementing updating mount point
err = errtypes.NotSupported("gateway: update of mount point is not yet implemented")
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewUnimplemented(ctx, err, "error updating received share"),
}, nil
default:
return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported")
}
}
// handle transfer in case it has not already been accepted
if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED {
if share.State == ocm.ShareState_SHARE_STATE_ACCEPTED {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare, share already accepted.")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_FAILED_PRECONDITION,
Message: "Share already accepted.",
},
}, err
}
// get provided destination path
transferDestinationPath, err := s.getTransferDestinationPath(ctx, req)
if err != nil {
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, err
}
}
error := s.handleTransfer(ctx, share, transferDestinationPath)
if error != nil {
log.Err(error).Msg("gateway: error handling transfer in UpdateReceivedOCMShare")
return &ocm.UpdateReceivedOCMShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, error
}
}
return res, nil
}
func (s *svc) handleTransfer(ctx context.Context, share *ocm.ReceivedShare, transferDestinationPath string) error {
log := appctx.GetLogger(ctx)
protocol, ok := s.getTransferProtocol(share)
if !ok {
return errors.New("gateway: unable to retrieve transfer protocol")
}
sourceURI := protocol.Uri
// get the webdav endpoint of the grantee's idp
var granteeIdp string
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER {
granteeIdp = share.GetGrantee().GetUserId().Idp
}
if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP {
granteeIdp = share.GetGrantee().GetGroupId().Idp
}
destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return err
}
destWebdavEndpointURL, err := url.Parse(destWebdavEndpoint)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare: unable to parse webdav endpoint \"" + destWebdavEndpoint + "\" into URL structure")
return err
}
destWebdavHost, err := s.getWebdavHost(ctx, granteeIdp)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare")
return err
}
var dstWebdavURLString string
if strings.Contains(destWebdavHost, "://") {
dstWebdavURLString = destWebdavHost
} else {
dstWebdavURLString = "http://" + destWebdavHost
}
dstWebdavHostURL, err := url.Parse(dstWebdavURLString)
if err != nil {
log.Err(err).Msg("gateway: error calling UpdateReceivedOCMShare: unable to parse webdav service host \"" + dstWebdavURLString + "\" into URL structure")
return err
}
destServiceHost := dstWebdavHostURL.Host + dstWebdavHostURL.Path
// optional prefix must only appear in target url path:
// http://...token...@reva.eu/prefix/?name=remote.php/webdav/home/...
destEndpointPath := strings.TrimPrefix(destWebdavEndpointURL.Path, dstWebdavHostURL.Path)
destEndpointScheme := destWebdavEndpointURL.Scheme
destToken := ctxpkg.ContextMustGetToken(ctx)
destPath := path.Join(destEndpointPath, transferDestinationPath, path.Base(share.Name))
destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destServiceHost, destPath)
// var destUri string
req := &datatx.CreateTransferRequest{
SrcTargetUri: sourceURI,
DestTargetUri: destTargetURI,
ShareId: share.Id,
}
res, err := s.CreateTransfer(ctx, req)
if err != nil {
return err
}
log.Info().Msgf("gateway: CreateTransfer: %v", res.TxInfo)
return nil
}
func (s *svc) isTransferShare(share *ocm.ReceivedShare) bool {
_, ok := s.getTransferProtocol(share)
return ok
}
func (s *svc) getTransferDestinationPath(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (string, error) {
log := appctx.GetLogger(ctx)
// the destination path is not part of any protocol, but an opaque field
destPathOpaque, ok := req.GetOpaque().GetMap()["transfer_destination_path"]
if ok {
switch destPathOpaque.Decoder {
case "plain":
if string(destPathOpaque.Value) != "" {
return string(destPathOpaque.Value), nil
}
default:
return "", errtypes.NotSupported("decoder of opaque entry 'transfer_destination_path' not recognized: " + destPathOpaque.Decoder)
}
}
log.Info().Msg("destination path not provided, trying default transfer destination folder")
if s.c.DataTransfersFolder == "" {
return "", errtypes.NotSupported("no destination path provided and default transfer destination folder is not set")
}
return s.c.DataTransfersFolder, nil
}
func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient")
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.GetReceivedOCMShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetReceivedOCMShare")
}
return res, nil
}
func (s *svc) getTransferProtocol(share *ocm.ReceivedShare) (*ocm.WebDAVProtocol, bool) {
for _, p := range share.Protocols {
if d, ok := p.Term.(*ocm.Protocol_WebdavOptions); ok {
if slices.Contains(d.WebdavOptions.AccessTypes, ocm.AccessType_ACCESS_TYPE_DATATX) {
return d.WebdavOptions, true
}
}
}
return nil, false
}
func (s *svc) CreateOCMIncomingShare(context.Context, *incoming.CreateOCMIncomingShareRequest) (*incoming.CreateOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: CreateOCMIncomingShare is not supported")
}
func (s *svc) UpdateOCMIncomingShare(context.Context, *incoming.UpdateOCMIncomingShareRequest) (*incoming.UpdateOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: UpdateOCMIncomingShare is not supported")
}
func (s *svc) DeleteOCMIncomingShare(context.Context, *incoming.DeleteOCMIncomingShareRequest) (*incoming.DeleteOCMIncomingShareResponse, error) {
return nil, errtypes.NotSupported("gateway: DeleteOCMIncomingShare is not supported")
}
func (s *svc) ListExistingOCMShares(context.Context, *ocm.ListOCMSharesRequest) (*gateway.ListExistingOCMSharesResponse, error) {
return nil, errtypes.NotSupported("gateway: ListExistingOCMShares is not supported")
}
@@ -0,0 +1,37 @@
// Copyright 2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
func (s *svc) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest) (*permissions.CheckPermissionResponse, error) {
c, err := pool.GetPermissionsClient(s.c.PermissionsEndpoint)
if err != nil {
return &permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, "error getting permissions client"),
}, nil
}
return c.CheckPermission(ctx, req)
}
@@ -0,0 +1,60 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
preferences "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) SetKey(ctx context.Context, req *preferences.SetKeyRequest) (*preferences.SetKeyResponse, error) {
c, err := pool.GetPreferencesClient(s.c.PreferencesEndpoint)
if err != nil {
return &preferences.SetKeyResponse{
Status: status.NewInternal(ctx, "error getting preferences client"),
}, nil
}
res, err := c.SetKey(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling SetKey")
}
return res, nil
}
func (s *svc) GetKey(ctx context.Context, req *preferences.GetKeyRequest) (*preferences.GetKeyResponse, error) {
c, err := pool.GetPreferencesClient(s.c.PreferencesEndpoint)
if err != nil {
return &preferences.GetKeyResponse{
Status: status.NewInternal(ctx, "error getting preferences client"),
}, nil
}
res, err := c.GetKey(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetKey")
}
return res, nil
}
@@ -0,0 +1,131 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) CreatePublicShare(ctx context.Context, req *link.CreatePublicShareRequest) (*link.CreatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("create public share")
c, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
return c.CreatePublicShare(ctx, req)
}
func (s *svc) RemovePublicShare(ctx context.Context, req *link.RemovePublicShareRequest) (*link.RemovePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("remove public share")
driver, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
return driver.RemovePublicShare(ctx, req)
}
func (s *svc) GetPublicShareByToken(ctx context.Context, req *link.GetPublicShareByTokenRequest) (*link.GetPublicShareByTokenResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("get public share by token")
driver, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
return nil, err
}
res, err := driver.GetPublicShareByToken(ctx, req)
if err != nil {
return nil, err
}
return res, nil
}
func (s *svc) GetPublicShare(ctx context.Context, req *link.GetPublicShareRequest) (*link.GetPublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("get public share")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.GetPublicShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
return pClient.GetPublicShare(ctx, req)
}
func (s *svc) ListPublicShares(ctx context.Context, req *link.ListPublicSharesRequest) (*link.ListPublicSharesResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("listing public shares")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.ListPublicSharesResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
res, err := pClient.ListPublicShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "error listing shares")
}
return res, nil
}
func (s *svc) ListExistingPublicShares(_ context.Context, _ *link.ListPublicSharesRequest) (*gateway.ListExistingPublicSharesResponse, error) {
return nil, errtypes.NotSupported("method ListExistingPublicShares not implemented")
}
func (s *svc) UpdatePublicShare(ctx context.Context, req *link.UpdatePublicShareRequest) (*link.UpdatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Msg("update public share")
pClient, err := pool.GetPublicShareProviderClient(s.c.PublicShareProviderEndpoint)
if err != nil {
log.Err(err).Msg("error connecting to a public share provider")
return &link.UpdatePublicShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
return pClient.UpdatePublicShare(ctx, req)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,272 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/grpc"
)
/*
Cached Registry
*/
type cachedRegistryClient struct {
c registry.RegistryAPIClient
cache cache.ProviderCache
}
func (c *cachedRegistryClient) ListStorageProviders(ctx context.Context, in *registry.ListStorageProvidersRequest, opts ...grpc.CallOption) (*registry.ListStorageProvidersResponse, error) {
spaceID := sdk.DecodeOpaqueMap(in.Opaque)["space_id"]
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.cache.GetKey(u.GetId(), spaceID)
if key != "" {
s := &registry.ListStorageProvidersResponse{}
if err := c.cache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.ListStorageProviders(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK:
return resp, nil
case spaceID == "":
return resp, nil
case spaceID == utils.ShareStorageSpaceID: // TODO do we need to compare providerid and spaceid separately?
return resp, nil
default:
return resp, c.cache.PushToCache(key, resp)
}
}
// not cached
func (c *cachedRegistryClient) GetStorageProviders(ctx context.Context, in *registry.GetStorageProvidersRequest, opts ...grpc.CallOption) (*registry.GetStorageProvidersResponse, error) {
return c.c.GetStorageProviders(ctx, in, opts...)
}
func (c *cachedRegistryClient) GetHome(ctx context.Context, in *registry.GetHomeRequest, opts ...grpc.CallOption) (*registry.GetHomeResponse, error) {
return c.c.GetHome(ctx, in, opts...)
}
/*
Cached Spaces Provider
*/
type cachedSpacesAPIClient struct {
c provider.SpacesAPIClient
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// CreateStorageSpace creates a storage space
func (c *cachedSpacesAPIClient) CreateStorageSpace(ctx context.Context, in *provider.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.CreateStorageSpaceResponse, error) {
if in.Type == "personal" {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.createPersonalSpaceCache.GetKey(u.GetId())
if key != "" {
s := &provider.CreateStorageSpaceResponse{}
if err := c.createPersonalSpaceCache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.CreateStorageSpace(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK && resp.Status.Code != rpc.Code_CODE_ALREADY_EXISTS:
return resp, nil
case key == "":
return resp, nil
default:
return resp, c.createPersonalSpaceCache.PushToCache(key, resp)
}
}
return c.c.CreateStorageSpace(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) ListStorageSpaces(ctx context.Context, in *provider.ListStorageSpacesRequest, opts ...grpc.CallOption) (*provider.ListStorageSpacesResponse, error) {
return c.c.ListStorageSpaces(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) UpdateStorageSpace(ctx context.Context, in *provider.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.UpdateStorageSpaceResponse, error) {
return c.c.UpdateStorageSpace(ctx, in, opts...)
}
func (c *cachedSpacesAPIClient) DeleteStorageSpace(ctx context.Context, in *provider.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*provider.DeleteStorageSpaceResponse, error) {
return c.c.DeleteStorageSpace(ctx, in, opts...)
}
/*
Cached Storage Provider
*/
type cachedAPIClient struct {
c provider.ProviderAPIClient
createPersonalSpaceCache cache.CreatePersonalSpaceCache
}
// CreateHome caches calls to CreateHome locally - anyways they only need to be called once per user
func (c *cachedAPIClient) CreateHome(ctx context.Context, in *provider.CreateHomeRequest, opts ...grpc.CallOption) (*provider.CreateHomeResponse, error) {
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
key := c.createPersonalSpaceCache.GetKey(u.GetId())
if key != "" {
s := &provider.CreateHomeResponse{}
if err := c.createPersonalSpaceCache.PullFromCache(key, s); err == nil {
return s, nil
}
}
resp, err := c.c.CreateHome(ctx, in, opts...)
switch {
case err != nil:
return nil, err
case resp.Status.Code != rpc.Code_CODE_OK && resp.Status.Code != rpc.Code_CODE_ALREADY_EXISTS:
return resp, nil
case key == "":
return resp, nil
default:
return resp, c.createPersonalSpaceCache.PushToCache(key, resp)
}
}
// methods below here are not cached, they just call the client directly
// Stat returns the Resoure info for a given resource
func (c *cachedAPIClient) Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error) {
return c.c.Stat(ctx, in, opts...)
}
func (c *cachedAPIClient) AddGrant(ctx context.Context, in *provider.AddGrantRequest, opts ...grpc.CallOption) (*provider.AddGrantResponse, error) {
return c.c.AddGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateContainer(ctx context.Context, in *provider.CreateContainerRequest, opts ...grpc.CallOption) (*provider.CreateContainerResponse, error) {
return c.c.CreateContainer(ctx, in, opts...)
}
func (c *cachedAPIClient) Delete(ctx context.Context, in *provider.DeleteRequest, opts ...grpc.CallOption) (*provider.DeleteResponse, error) {
return c.c.Delete(ctx, in, opts...)
}
func (c *cachedAPIClient) DenyGrant(ctx context.Context, in *provider.DenyGrantRequest, opts ...grpc.CallOption) (*provider.DenyGrantResponse, error) {
return c.c.DenyGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) GetPath(ctx context.Context, in *provider.GetPathRequest, opts ...grpc.CallOption) (*provider.GetPathResponse, error) {
return c.c.GetPath(ctx, in, opts...)
}
func (c *cachedAPIClient) GetQuota(ctx context.Context, in *provider.GetQuotaRequest, opts ...grpc.CallOption) (*provider.GetQuotaResponse, error) {
return c.c.GetQuota(ctx, in, opts...)
}
func (c *cachedAPIClient) InitiateFileDownload(ctx context.Context, in *provider.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*provider.InitiateFileDownloadResponse, error) {
return c.c.InitiateFileDownload(ctx, in, opts...)
}
func (c *cachedAPIClient) InitiateFileUpload(ctx context.Context, in *provider.InitiateFileUploadRequest, opts ...grpc.CallOption) (*provider.InitiateFileUploadResponse, error) {
return c.c.InitiateFileUpload(ctx, in, opts...)
}
func (c *cachedAPIClient) ListGrants(ctx context.Context, in *provider.ListGrantsRequest, opts ...grpc.CallOption) (*provider.ListGrantsResponse, error) {
return c.c.ListGrants(ctx, in, opts...)
}
func (c *cachedAPIClient) ListContainerStream(ctx context.Context, in *provider.ListContainerStreamRequest, opts ...grpc.CallOption) (provider.ProviderAPI_ListContainerStreamClient, error) {
return c.c.ListContainerStream(ctx, in, opts...)
}
func (c *cachedAPIClient) ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error) {
return c.c.ListContainer(ctx, in, opts...)
}
func (c *cachedAPIClient) ListFileVersions(ctx context.Context, in *provider.ListFileVersionsRequest, opts ...grpc.CallOption) (*provider.ListFileVersionsResponse, error) {
return c.c.ListFileVersions(ctx, in, opts...)
}
func (c *cachedAPIClient) ListRecycleStream(ctx context.Context, in *provider.ListRecycleStreamRequest, opts ...grpc.CallOption) (provider.ProviderAPI_ListRecycleStreamClient, error) {
return c.c.ListRecycleStream(ctx, in, opts...)
}
func (c *cachedAPIClient) ListRecycle(ctx context.Context, in *provider.ListRecycleRequest, opts ...grpc.CallOption) (*provider.ListRecycleResponse, error) {
return c.c.ListRecycle(ctx, in, opts...)
}
func (c *cachedAPIClient) Move(ctx context.Context, in *provider.MoveRequest, opts ...grpc.CallOption) (*provider.MoveResponse, error) {
return c.c.Move(ctx, in, opts...)
}
func (c *cachedAPIClient) RemoveGrant(ctx context.Context, in *provider.RemoveGrantRequest, opts ...grpc.CallOption) (*provider.RemoveGrantResponse, error) {
return c.c.RemoveGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) PurgeRecycle(ctx context.Context, in *provider.PurgeRecycleRequest, opts ...grpc.CallOption) (*provider.PurgeRecycleResponse, error) {
return c.c.PurgeRecycle(ctx, in, opts...)
}
func (c *cachedAPIClient) RestoreFileVersion(ctx context.Context, in *provider.RestoreFileVersionRequest, opts ...grpc.CallOption) (*provider.RestoreFileVersionResponse, error) {
return c.c.RestoreFileVersion(ctx, in, opts...)
}
func (c *cachedAPIClient) RestoreRecycleItem(ctx context.Context, in *provider.RestoreRecycleItemRequest, opts ...grpc.CallOption) (*provider.RestoreRecycleItemResponse, error) {
return c.c.RestoreRecycleItem(ctx, in, opts...)
}
func (c *cachedAPIClient) UpdateGrant(ctx context.Context, in *provider.UpdateGrantRequest, opts ...grpc.CallOption) (*provider.UpdateGrantResponse, error) {
return c.c.UpdateGrant(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateSymlink(ctx context.Context, in *provider.CreateSymlinkRequest, opts ...grpc.CallOption) (*provider.CreateSymlinkResponse, error) {
return c.c.CreateSymlink(ctx, in, opts...)
}
func (c *cachedAPIClient) CreateReference(ctx context.Context, in *provider.CreateReferenceRequest, opts ...grpc.CallOption) (*provider.CreateReferenceResponse, error) {
return c.c.CreateReference(ctx, in, opts...)
}
func (c *cachedAPIClient) SetArbitraryMetadata(ctx context.Context, in *provider.SetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.SetArbitraryMetadataResponse, error) {
return c.c.SetArbitraryMetadata(ctx, in, opts...)
}
func (c *cachedAPIClient) UnsetArbitraryMetadata(ctx context.Context, in *provider.UnsetArbitraryMetadataRequest, opts ...grpc.CallOption) (*provider.UnsetArbitraryMetadataResponse, error) {
return c.c.UnsetArbitraryMetadata(ctx, in, opts...)
}
func (c *cachedAPIClient) SetLock(ctx context.Context, in *provider.SetLockRequest, opts ...grpc.CallOption) (*provider.SetLockResponse, error) {
return c.c.SetLock(ctx, in, opts...)
}
func (c *cachedAPIClient) GetLock(ctx context.Context, in *provider.GetLockRequest, opts ...grpc.CallOption) (*provider.GetLockResponse, error) {
return c.c.GetLock(ctx, in, opts...)
}
func (c *cachedAPIClient) RefreshLock(ctx context.Context, in *provider.RefreshLockRequest, opts ...grpc.CallOption) (*provider.RefreshLockResponse, error) {
return c.c.RefreshLock(ctx, in, opts...)
}
func (c *cachedAPIClient) Unlock(ctx context.Context, in *provider.UnlockRequest, opts ...grpc.CallOption) (*provider.UnlockResponse, error) {
return c.c.Unlock(ctx, in, opts...)
}
func (c *cachedAPIClient) GetHome(ctx context.Context, in *provider.GetHomeRequest, opts ...grpc.CallOption) (*provider.GetHomeResponse, error) {
return c.c.GetHome(ctx, in, opts...)
}
func (c *cachedAPIClient) TouchFile(ctx context.Context, in *provider.TouchFileRequest, opts ...grpc.CallOption) (*provider.TouchFileResponse, error) {
return c.c.TouchFile(ctx, in, opts...)
}
func (c *cachedAPIClient) AddLabel(ctx context.Context, in *provider.AddLabelRequest, opts ...grpc.CallOption) (*provider.AddLabelResponse, error) {
return c.c.AddLabel(ctx, in, opts...)
}
func (c *cachedAPIClient) RemoveLabel(ctx context.Context, in *provider.RemoveLabelRequest, opts ...grpc.CallOption) (*provider.RemoveLabelResponse, error) {
return c.c.RemoveLabel(ctx, in, opts...)
}
@@ -0,0 +1,61 @@
// Copyright 2018-2021 CERN
// Copyright 2026 OpenCloud GmbH
//
// 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 gateway
import (
"context"
tenant "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetTenant(ctx context.Context, req *tenant.GetTenantRequest) (*tenant.GetTenantResponse, error) {
c, err := pool.GetTenantProviderServiceClient(s.c.TenantProviderEndpoint)
if err != nil {
return &tenant.GetTenantResponse{
Status: status.NewInternal(ctx, "error getting tenant service client"),
}, nil
}
res, err := c.GetTenant(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTenant")
}
return res, nil
}
func (s *svc) GetTenantByClaim(ctx context.Context, req *tenant.GetTenantByClaimRequest) (*tenant.GetTenantByClaimResponse, error) {
c, err := pool.GetTenantProviderServiceClient(s.c.TenantProviderEndpoint)
if err != nil {
return &tenant.GetTenantByClaimResponse{
Status: status.NewInternal(ctx, "error getting tenant service client"),
}, nil
}
res, err := c.GetTenantByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetTenantByClaim")
}
return res, nil
}
@@ -0,0 +1,92 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (s *svc) GetUser(ctx context.Context, req *user.GetUserRequest) (*user.GetUserResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUser(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUser")
}
return res, nil
}
func (s *svc) GetUserByClaim(ctx context.Context, req *user.GetUserByClaimRequest) (*user.GetUserByClaimResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserByClaimResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUserByClaim(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserByClaim")
}
return res, nil
}
func (s *svc) FindUsers(ctx context.Context, req *user.FindUsersRequest) (*user.FindUsersResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.FindUsersResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.FindUsers(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling FindUsers")
}
return res, nil
}
func (s *svc) GetUserGroups(ctx context.Context, req *user.GetUserGroupsRequest) (*user.GetUserGroupsResponse, error) {
c, err := pool.GetUserProviderServiceClient(s.c.UserProviderEndpoint)
if err != nil {
return &user.GetUserGroupsResponse{
Status: status.NewInternal(ctx, "error getting auth client"),
}, nil
}
res, err := c.GetUserGroups(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetUserGroups")
}
return res, nil
}
@@ -0,0 +1,644 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package gateway
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/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"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
)
// TODO(labkode): add multi-phase commit logic when commit share or commit ref is enabled.
func (s *svc) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
return s.addShare(ctx, req)
}
func (s *svc) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
return s.removeShare(ctx, req)
}
func (s *svc) UpdateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
return s.updateShare(ctx, req)
}
// TODO(labkode): we need to validate share state vs storage grant and storage ref
// If there are any inconsistencies, the share needs to be flag as invalid and a background process
// or active fix needs to be performed.
func (s *svc) GetShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
return s.getShare(ctx, req)
}
func (s *svc) getShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("getShare: failed to get user share provider")
return &collaboration.GetShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.GetShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetShare")
}
return res, nil
}
// TODO(labkode): read GetShare comment.
func (s *svc) ListShares(ctx context.Context, req *collaboration.ListSharesRequest) (*collaboration.ListSharesResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("ListShares: failed to get user share provider")
return &collaboration.ListSharesResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
res, err := c.ListShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListShares")
}
return res, nil
}
func (s *svc) ListExistingShares(_ context.Context, _ *collaboration.ListSharesRequest) (*gateway.ListExistingSharesResponse, error) {
return nil, errtypes.NotSupported("method ListExistingShares not implemented")
}
func (s *svc) updateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
// TODO: update wopi server
// FIXME This is a workaround that should prevent removing or changing the share permissions when the file is locked.
// https://github.com/owncloud/ocis/issues/8474
if status, err := s.checkLock(ctx, req.GetShare().GetId()); err != nil {
return &collaboration.UpdateShareResponse{
Status: status,
}, nil
}
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateShare: failed to get user share provider")
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.UpdateShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateShare")
}
if res.GetStatus().GetCode() != rpc.Code_CODE_OK {
return res, nil
}
if s.c.CommitShareToStorageGrant {
creator, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
grant := &provider.Grant{
Grantee: res.GetShare().GetGrantee(),
Permissions: res.GetShare().GetPermissions().GetPermissions(),
Expiration: res.GetShare().GetExpiration(),
Creator: creator.GetId(),
}
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(res.GetShare().GetResourceId()) {
opaque = utils.SpaceGrantOpaque()
utils.AppendPlainToOpaque(opaque, "spacetype", utils.ReadPlainFromOpaque(req.GetOpaque(), "spacetype"))
}
updateGrantStatus, err := s.updateGrant(ctx, res.GetShare().GetResourceId(), grant, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling updateGrant")
}
if updateGrantStatus.Code != rpc.Code_CODE_OK {
return &collaboration.UpdateShareResponse{
Status: updateGrantStatus,
Share: res.GetShare(),
}, nil
}
}
return res, nil
}
// TODO(labkode): listing received shares just goes to the user share manager and gets the list of
// received shares. The display name of the shares should be the a friendly name, like the basename
// of the original file.
func (s *svc) ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest) (*collaboration.ListReceivedSharesResponse, error) {
logger := appctx.GetLogger(ctx)
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
logger.Error().
Err(err).
Msg("ListReceivedShares: failed to get user share provider")
return &collaboration.ListReceivedSharesResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
res, err := c.ListReceivedShares(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling ListReceivedShares")
}
return res, nil
}
func (s *svc) GetReceivedShare(ctx context.Context, req *collaboration.GetReceivedShareRequest) (*collaboration.GetReceivedShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("GetReceivedShare: failed to get user share provider")
return &collaboration.GetReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
res, err := c.GetReceivedShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetReceivedShare")
}
return res, nil
}
// When updating a received share:
// if the update contains update for displayName:
// 1. if received share is mounted: we also do a rename in the storage
// 2. if received share is not mounted: we only rename in user share provider.
func (s *svc) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) {
ctx, span := appctx.GetTracerProvider(ctx).Tracer("gateway").Start(ctx, "Gateway.UpdateReceivedShare")
defer span.End()
// sanity checks
switch {
case req.GetShare() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "updating requires a received share object"),
}, nil
case req.GetShare().GetShare() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share missing"),
}, nil
case req.GetShare().GetShare().GetId() == nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id missing"),
}, nil
case req.GetShare().GetShare().GetId().GetOpaqueId() == "":
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id empty"),
}, nil
}
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateReceivedShare: failed to get user share provider")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
return c.UpdateReceivedShare(ctx, req)
/*
TODO: Leftover from master merge. Do we need this?
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("UpdateReceivedShare: failed to get user share provider")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting share provider client"),
}, nil
}
// check if we have a resource id in the update response that we can use to update references
if res.GetShare().GetShare().GetResourceId() == nil {
log.Err(err).Msg("gateway: UpdateReceivedShare must return a ResourceId")
return &collaboration.UpdateReceivedShareResponse{
Status: &rpc.Status{
Code: rpc.Code_CODE_INTERNAL,
},
}, nil
}
// properties are updated in the order they appear in the field mask
// when an error occurs the request ends and no further fields are updated
for i := range req.UpdateMask.Paths {
switch req.UpdateMask.Paths[i] {
case "state":
switch req.GetShare().GetState() {
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
rpcStatus := s.createReference(ctx, res.GetShare().GetShare().GetResourceId())
if rpcStatus.Code != rpc.Code_CODE_OK {
return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil
}
case collaboration.ShareState_SHARE_STATE_REJECTED:
rpcStatus := s.removeReference(ctx, res.GetShare().GetShare().ResourceId)
if rpcStatus.Code != rpc.Code_CODE_OK && rpcStatus.Code != rpc.Code_CODE_NOT_FOUND {
return &collaboration.UpdateReceivedShareResponse{Status: rpcStatus}, nil
}
}
case "mount_point":
// TODO(labkode): implementing updating mount point
err = errtypes.NotSupported("gateway: update of mount point is not yet implemented")
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewUnimplemented(ctx, err, "error updating received share"),
}, nil
default:
return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported")
}
}
return res, nil
*/
}
func (s *svc) ListExistingReceivedShares(_ context.Context, _ *collaboration.ListReceivedSharesRequest) (*gateway.ListExistingReceivedSharesResponse, error) {
return nil, errtypes.NotSupported("Unimplemented")
}
func (s *svc) denyGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.DenyGrantRequest{
Ref: ref,
Grantee: g,
Opaque: opaque,
// TODO add creator
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("denyGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.DenyGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling DenyGrant")
}
return grantRes.Status, nil
}
func (s *svc) addGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions, expiration *typesv1beta1.Timestamp, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
creator, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return nil, errors.New("user not found in context")
}
grantReq := &provider.AddGrantRequest{
Ref: ref,
Grant: &provider.Grant{
Grantee: g,
Permissions: p,
Creator: creator.GetId(),
Expiration: expiration,
},
Opaque: opaque,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("addGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.AddGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling AddGrant")
}
return grantRes.Status, nil
}
func (s *svc) updateGrant(ctx context.Context, id *provider.ResourceId, grant *provider.Grant, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.UpdateGrantRequest{
Opaque: opaque,
Ref: ref,
Grant: grant,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("updateGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.UpdateGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateGrant")
}
if grantRes.Status.Code != rpc.Code_CODE_OK {
return status.NewInternal(ctx,
"error committing share to storage grant"), nil
}
return status.NewOK(ctx), nil
}
func (s *svc) removeGrant(ctx context.Context, id *provider.ResourceId, g *provider.Grantee, p *provider.ResourcePermissions, opaque *typesv1beta1.Opaque) (*rpc.Status, error) {
ref := &provider.Reference{
ResourceId: id,
}
grantReq := &provider.RemoveGrantRequest{
Ref: ref,
Grant: &provider.Grant{
Grantee: g,
Permissions: p,
},
Opaque: opaque,
}
c, _, err := s.find(ctx, ref)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Interface("reference", ref).
Msg("removeGrant: failed to get storage provider")
if _, ok := err.(errtypes.IsNotFound); ok {
return status.NewNotFound(ctx, "storage provider not found"), nil
}
return status.NewInternal(ctx, "error finding storage provider"), nil
}
grantRes, err := c.RemoveGrant(ctx, grantReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveGrant")
}
if grantRes.Status.Code != rpc.Code_CODE_OK {
return grantRes.GetStatus(), nil
}
return status.NewOK(ctx), nil
}
func (s *svc) addShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("CreateShare: failed to get user share provider")
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "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")
}
if res.Status.Code != rpc.Code_CODE_OK {
return res, nil
}
rollBackFn := func(status *rpc.Status) {
rmvReq := &collaboration.RemoveShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Key{
Key: &collaboration.ShareKey{
ResourceId: req.ResourceInfo.Id,
Grantee: req.Grant.Grantee,
},
},
},
}
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("rollback the CreateShare attempt")
if resp, err := s.removeShare(ctx, rmvReq); err != nil {
appctx.GetLogger(ctx).Debug().Interface("status", resp.GetStatus()).Interface("req", req).Msg(err.Error())
}
}
if s.c.CommitShareToStorageGrant {
// If the share is a denial we call denyGrant instead.
var status *rpc.Status
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(req.ResourceInfo.Id) {
opaque = utils.SpaceGrantOpaque()
utils.AppendPlainToOpaque(opaque, "spacetype", req.ResourceInfo.GetSpace().GetSpaceType())
}
if grants.PermissionsEqual(req.Grant.Permissions.Permissions, &provider.ResourcePermissions{}) {
status, err = s.denyGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error denying grant in storage")
}
} else {
status, err = s.addGrant(ctx, req.ResourceInfo.Id, req.Grant.Grantee, req.Grant.Permissions.Permissions, req.Grant.Expiration, opaque)
if err != nil {
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error())
rollBackFn(status)
return nil, errors.Wrap(err, "gateway: error adding grant to storage")
}
}
switch status.Code {
case rpc.Code_CODE_OK, rpc.Code_CODE_ALREADY_EXISTS:
// ok
case rpc.Code_CODE_UNIMPLEMENTED:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring")
rollBackFn(status)
default:
appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful")
rollBackFn(status)
return &collaboration.CreateShareResponse{
Status: status,
}, err
}
}
return res, nil
}
func (s *svc) removeShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
c, err := pool.GetUserShareProviderClient(s.c.UserShareProviderEndpoint)
if err != nil {
appctx.GetLogger(ctx).
Err(err).
Msg("RemoveShare: failed to get user share provider")
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error getting user share provider client"),
}, nil
}
// if we need to commit the share, we need the resource it points to.
var share *collaboration.Share
// FIXME: I will cause a panic as share will be nil when I'm false
if s.c.CommitShareToStorageGrant {
getShareReq := &collaboration.GetShareRequest{
Ref: req.Ref,
}
getShareRes, err := c.GetShare(ctx, getShareReq)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling GetShare")
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx,
"error getting share when committing to the storage"),
}
return res, nil
}
share = getShareRes.Share
}
// TODO: update wopi server
// FIXME This is a workaround that should prevent removing or changing the share permissions when the file is locked.
// https://github.com/owncloud/ocis/issues/8474
if status, err := s.checkShareLock(ctx, share); err != nil {
return &collaboration.RemoveShareResponse{
Status: status,
}, nil
}
res, err := c.RemoveShare(ctx, req)
if err != nil {
return nil, errors.Wrap(err, "gateway: error calling RemoveShare")
}
if res.Status.Code != rpc.Code_CODE_OK {
return res, nil
}
if s.c.CommitShareToStorageGrant {
var opaque *typesv1beta1.Opaque
if refIsSpaceRoot(share.ResourceId) {
opaque = utils.SpaceGrantOpaque()
}
removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, opaque)
if err != nil {
return nil, errors.Wrap(err, "gateway: error removing grant from storage")
}
if removeGrantStatus.Code != rpc.Code_CODE_OK {
return &collaboration.RemoveShareResponse{
Status: removeGrantStatus,
}, err
}
}
return res, nil
}
func (s *svc) checkLock(ctx context.Context, shareId *collaboration.ShareId) (*rpc.Status, error) {
logger := appctx.GetLogger(ctx)
getShareRes, err := s.GetShare(ctx, &collaboration.GetShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{Id: shareId},
},
})
if err != nil {
msg := "gateway: error calling GetShare"
logger.Err(err).Interface("share_id", shareId).Msg(msg)
return status.NewInternal(ctx, msg), errors.Wrap(err, msg)
}
if getShareRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
msg := "can not get share stat " + getShareRes.GetStatus().GetMessage()
logger.Debug().Interface("share", shareId).Msg(msg)
if getShareRes.GetStatus().GetCode() != rpc.Code_CODE_NOT_FOUND {
return status.NewNotFound(ctx, msg), errors.New(msg)
}
return status.NewInternal(ctx, msg), errors.New(msg)
}
return s.checkShareLock(ctx, getShareRes.Share)
}
func (s *svc) checkShareLock(ctx context.Context, share *collaboration.Share) (*rpc.Status, error) {
logger := appctx.GetLogger(ctx)
sRes, err := s.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.GetResourceId()},
ArbitraryMetadataKeys: []string{"lockdiscovery"}})
if err != nil {
msg := "failed to stat shared resource"
logger.Err(err).Interface("resource_id", share.GetResourceId()).Msg(msg)
return status.NewInternal(ctx, msg), errors.Wrap(err, msg)
}
if sRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
msg := "can not get share stat " + sRes.GetStatus().GetMessage()
logger.Debug().Interface("lock", sRes.GetInfo().GetLock()).Msg(msg)
if sRes.GetStatus().GetCode() != rpc.Code_CODE_NOT_FOUND {
return status.NewNotFound(ctx, msg), errors.New(msg)
}
return status.NewInternal(ctx, msg), errors.New(msg)
}
if sRes.GetInfo().GetLock() != nil {
msg := "can not change grants, the shared resource is locked"
logger.Debug().Interface("lock", sRes.GetInfo().GetLock()).Msg(msg)
return status.NewLocked(ctx, msg), errors.New(msg)
}
return nil, nil
}
func refIsSpaceRoot(ref *provider.ResourceId) bool {
if ref == nil {
return false
}
if ref.SpaceId == "" || ref.OpaqueId == "" {
return false
}
return ref.SpaceId == ref.OpaqueId
}
@@ -0,0 +1,106 @@
// 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 gateway
import (
"context"
"net/url"
"path"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
)
type webdavEndpoint struct {
filePath string
endpoint string
token string
}
func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webdavEndpoint, error) {
if targetURL == "" {
return nil, errtypes.BadRequest("gateway: ref target is an empty uri")
}
uri, err := url.Parse(targetURL)
if err != nil {
return nil, errors.Wrap(err, "gateway: error parsing target uri: "+targetURL)
}
if uri.Scheme != "webdav" {
return nil, errtypes.NotSupported("ref target does not have the webdav scheme")
}
m, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return nil, errors.Wrap(err, "gateway: error parsing target resource name")
}
return &webdavEndpoint{
filePath: m["name"][0],
endpoint: uri.Host,
token: uri.User.String(),
}, nil
}
func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
})
if err != nil {
return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
return s.Endpoint.Path, nil
}
}
return "", errtypes.NotFound(domain)
}
func (s *svc) getWebdavHost(ctx context.Context, domain string) (string, error) {
meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: domain,
})
if err != nil {
return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain")
}
for _, s := range meshProvider.ProviderInfo.Services {
if strings.ToLower(s.Endpoint.Type.Name) == "webdav" {
return s.Host, nil
}
}
return "", errtypes.NotFound(domain)
}
func appendNameQuery(targetURL string, nameQueries ...string) (string, error) {
uri, err := url.Parse(targetURL)
if err != nil {
return "", err
}
q, err := url.ParseQuery(uri.RawQuery)
if err != nil {
return "", err
}
name := append([]string{q["name"][0]}, nameQueries...)
q.Set("name", path.Join(name...))
uri.RawQuery = q.Encode()
return uri.String(), nil
}
@@ -0,0 +1,207 @@
// 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 groupprovider
import (
"context"
"fmt"
"sort"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("groupprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getDriver(c *config) (group.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for group manager", c.Driver))
}
// New returns a new GroupProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
groupManager, err := getDriver(c)
if err != nil {
return nil, err
}
svc := &service{groupmgr: groupManager}
return svc, nil
}
type service struct {
groupmgr group.Manager
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
grouppb.RegisterGroupAPIServer(ss, s)
}
func (s *service) GetGroup(ctx context.Context, req *grouppb.GetGroupRequest) (*grouppb.GetGroupResponse, error) {
if req.GroupId == nil {
res := &grouppb.GetGroupResponse{
Status: status.NewInvalid(ctx, "groupid missing"),
}
return res, nil
}
group, err := s.groupmgr.GetGroup(ctx, req.GroupId, req.SkipFetchingMembers)
if err != nil {
res := &grouppb.GetGroupResponse{}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "group not found")
} else {
res.Status = status.NewInternal(ctx, "error getting group")
}
return res, nil
}
return &grouppb.GetGroupResponse{
Status: status.NewOK(ctx),
Group: group,
}, nil
}
func (s *service) GetGroupByClaim(ctx context.Context, req *grouppb.GetGroupByClaimRequest) (*grouppb.GetGroupByClaimResponse, error) {
group, err := s.groupmgr.GetGroupByClaim(ctx, req.Claim, req.Value, req.SkipFetchingMembers)
if err != nil {
res := &grouppb.GetGroupByClaimResponse{}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, fmt.Sprintf("group not found %s %s", req.Claim, req.Value))
} else {
res.Status = status.NewInternal(ctx, "error getting group by claim")
}
return res, nil
}
return &grouppb.GetGroupByClaimResponse{
Status: status.NewOK(ctx),
Group: group,
}, nil
}
func (s *service) FindGroups(ctx context.Context, req *grouppb.FindGroupsRequest) (*grouppb.FindGroupsResponse, error) {
if len(req.Filters) > 1 || req.Filters[0].GetType() != grouppb.Filter_TYPE_QUERY {
return nil, fmt.Errorf("only one query filter supported")
}
groups, err := s.groupmgr.FindGroups(ctx, req.Filters[0].GetQuery(), req.SkipFetchingMembers)
if err != nil {
return &grouppb.FindGroupsResponse{
Status: status.NewInternal(ctx, "error finding groups"),
}, nil
}
// sort group by groupname
sort.Slice(groups, func(i, j int) bool {
return groups[i].GroupName <= groups[j].GroupName
})
return &grouppb.FindGroupsResponse{
Status: status.NewOK(ctx),
Groups: groups,
}, nil
}
func (s *service) GetMembers(ctx context.Context, req *grouppb.GetMembersRequest) (*grouppb.GetMembersResponse, error) {
if req.GroupId == nil {
res := &grouppb.GetMembersResponse{
Status: status.NewInvalid(ctx, "groupid missing"),
}
return res, nil
}
members, err := s.groupmgr.GetMembers(ctx, req.GroupId)
if err != nil {
return &grouppb.GetMembersResponse{
Status: status.NewInternal(ctx, "error getting group members"),
}, nil
}
return &grouppb.GetMembersResponse{
Status: status.NewOK(ctx),
Members: members,
}, nil
}
func (s *service) HasMember(ctx context.Context, req *grouppb.HasMemberRequest) (*grouppb.HasMemberResponse, error) {
if req.GroupId == nil || req.UserId == nil {
res := &grouppb.HasMemberResponse{
Status: status.NewInvalid(ctx, "groupid or userid missing"),
}
return res, nil
}
ok, err := s.groupmgr.HasMember(ctx, req.GroupId, req.UserId)
if err != nil {
return &grouppb.HasMemberResponse{
Status: status.NewInternal(ctx, "error checking for group member"),
}, nil
}
return &grouppb.HasMemberResponse{
Status: status.NewOK(ctx),
Ok: ok,
}, nil
}
@@ -0,0 +1,82 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package helloworld
import (
"context"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/grpc/services/helloworld/proto"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("helloworld", New)
}
type conf struct {
Message string `mapstructure:"message"`
}
type service struct {
conf *conf
}
// New returns a new PreferencesServiceServer
// It can be tested like this:
// prototool grpc --address 0.0.0.0:9999 --method 'revad.helloworld.HelloWorldService/Hello' --data '{"name": "Alice"}'
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c := &conf{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "helloworld: error decoding conf")
return nil, err
}
if c.Message == "" {
c.Message = "Hello"
}
service := &service{conf: c}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
proto.RegisterHelloWorldServiceServer(ss, s)
}
func (s *service) Hello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloResponse, error) {
if req.Name == "" {
req.Name = "Mr. Nobody"
}
message := fmt.Sprintf("%s %s", s.conf.Message, req.Name)
res := &proto.HelloResponse{
Message: message,
}
return res, nil
}
@@ -0,0 +1,224 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto
package proto
import (
context "context"
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_f5f9b8b18f6fb9a6, []int{0}
}
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (m *HelloRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloRequest.Merge(m, src)
}
func (m *HelloRequest) XXX_Size() int {
return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}
var xxx_messageInfo_HelloRequest proto.InternalMessageInfo
func (m *HelloRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
type HelloResponse struct {
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *HelloResponse) Reset() { *m = HelloResponse{} }
func (m *HelloResponse) String() string { return proto.CompactTextString(m) }
func (*HelloResponse) ProtoMessage() {}
func (*HelloResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_f5f9b8b18f6fb9a6, []int{1}
}
func (m *HelloResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_HelloResponse.Unmarshal(m, b)
}
func (m *HelloResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_HelloResponse.Marshal(b, m, deterministic)
}
func (m *HelloResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_HelloResponse.Merge(m, src)
}
func (m *HelloResponse) XXX_Size() int {
return xxx_messageInfo_HelloResponse.Size(m)
}
func (m *HelloResponse) XXX_DiscardUnknown() {
xxx_messageInfo_HelloResponse.DiscardUnknown(m)
}
var xxx_messageInfo_HelloResponse proto.InternalMessageInfo
func (m *HelloResponse) GetMessage() string {
if m != nil {
return m.Message
}
return ""
}
func init() {
proto.RegisterType((*HelloRequest)(nil), "revad.helloworld.HelloRequest")
proto.RegisterType((*HelloResponse)(nil), "revad.helloworld.HelloResponse")
}
func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_f5f9b8b18f6fb9a6) }
var fileDescriptor_f5f9b8b18f6fb9a6 = []byte{
// 162 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xce, 0x48, 0xcd, 0xc9,
0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0x29, 0x2e, 0x4b, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0x12, 0x28, 0x4a, 0x2d, 0x4b, 0x4c, 0xd1, 0x43, 0x48, 0x29, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78,
0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12,
0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, 0x26, 0x17, 0x2f, 0x54, 0x4d, 0x71, 0x41,
0x7e, 0x5e, 0x71, 0xaa, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, 0x62, 0x7a, 0xaa, 0x04,
0x13, 0x58, 0x1d, 0x8c, 0x6b, 0x14, 0xcb, 0x25, 0x08, 0x56, 0x1a, 0x0e, 0x32, 0x3c, 0x38, 0xb5,
0xa8, 0x2c, 0x33, 0x39, 0x55, 0xc8, 0x83, 0x8b, 0x15, 0x2c, 0x28, 0x24, 0xa7, 0x87, 0x6e, 0xbf,
0x1e, 0xb2, 0xe5, 0x52, 0xf2, 0x38, 0xe5, 0x21, 0x16, 0x3b, 0xb1, 0x47, 0xb1, 0x82, 0x3d, 0x92,
0xc4, 0x06, 0xa6, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0xe7, 0xab, 0x90, 0x7f, 0xe6, 0x00,
0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// HelloWorldServiceClient is the client API for HelloWorldService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type HelloWorldServiceClient interface {
Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}
type helloWorldServiceClient struct {
cc *grpc.ClientConn
}
func NewHelloWorldServiceClient(cc *grpc.ClientConn) HelloWorldServiceClient {
return &helloWorldServiceClient{cc}
}
func (c *helloWorldServiceClient) Hello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
out := new(HelloResponse)
err := c.cc.Invoke(ctx, "/revad.helloworld.HelloWorldService/Hello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HelloWorldServiceServer is the server API for HelloWorldService service.
type HelloWorldServiceServer interface {
Hello(context.Context, *HelloRequest) (*HelloResponse, error)
}
// UnimplementedHelloWorldServiceServer can be embedded to have forward compatible implementations.
type UnimplementedHelloWorldServiceServer struct {
}
func (*UnimplementedHelloWorldServiceServer) Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func RegisterHelloWorldServiceServer(s *grpc.Server, srv HelloWorldServiceServer) {
s.RegisterService(&_HelloWorldService_serviceDesc, srv)
}
func _HelloWorldService_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloWorldServiceServer).Hello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/revad.helloworld.HelloWorldService/Hello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloWorldServiceServer).Hello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
var _HelloWorldService_serviceDesc = grpc.ServiceDesc{
ServiceName: "revad.helloworld.HelloWorldService",
HandlerType: (*HelloWorldServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Hello",
Handler: _HelloWorldService_Hello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "helloworld.proto",
}
@@ -0,0 +1,35 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
syntax = "proto3";
package revad.helloworld;
option go_package = "proto";
service HelloWorldService {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 2;
}
@@ -0,0 +1,8 @@
generate:
go_options:
import_path: github.com/cs3org/reva/cmd/revad/svcs/grpcsvcs/helloworldsvc/proto
plugins:
- name : go
type: go
flags: plugins=grpc
output: ./
@@ -0,0 +1,46 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package loader
import (
// Load core gRPC services.
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/applicationauth"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/appprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/appregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/authprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/authregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/datatx"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/gateway"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/groupprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/helloworld"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmcore"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocminvitemanager"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmproviderauthorizer"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/ocmshareprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/permissions"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/preferences"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/publicshareprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/publicstorageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/sharesstorageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/storageprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/storageregistry"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/userprovider"
_ "github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider"
// Add your own service here
)
@@ -0,0 +1,148 @@
// 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 ocmcore
import (
"context"
"fmt"
"time"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmcore", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
repo share.Repository
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
ocmcore.RegisterOcmCoreAPIServer(ss, s)
}
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound(fmt.Sprintf("driver not found: %s", c.Driver))
}
// New creates a new ocm core svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
service := &service{
conf: &c,
repo: repo,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"}
}
// CreateOCMCoreShare is called when an OCM request comes into this reva instance from.
func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) {
if req.ShareType != ocm.ShareType_SHARE_TYPE_USER {
return nil, errtypes.NotSupported("share type not supported")
}
now := &typesv1beta1.Timestamp{
Seconds: uint64(time.Now().Unix()),
}
share, err := s.repo.StoreReceivedShare(ctx, &ocm.ReceivedShare{
RemoteShareId: req.ResourceId,
Name: req.Name,
Grantee: &providerpb.Grantee{
Type: providerpb.GranteeType_GRANTEE_TYPE_USER,
Id: &providerpb.Grantee_UserId{
UserId: req.ShareWith,
},
},
ResourceType: req.ResourceType,
ShareType: req.ShareType,
Owner: req.Owner,
Creator: req.Sender,
Protocols: req.Protocols,
Ctime: now,
Mtime: now,
Expiration: req.Expiration,
State: ocm.ShareState_SHARE_STATE_PENDING,
})
if err != nil {
// TODO: identify errors
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
return &ocmcore.CreateOCMCoreShareResponse{
Status: status.NewOK(ctx),
Id: share.Id.OpaqueId,
Created: share.Ctime,
}, nil
}
func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented")
}
@@ -0,0 +1,392 @@
// 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 ocminvitemanager
import (
"context"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/client"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocminvitemanager", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TokenExpiration string `mapstructure:"token_expiration"`
OCMClientTimeout int `mapstructure:"ocm_timeout"`
OCMClientInsecure bool `mapstructure:"ocm_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
tokenExpiration time.Duration
}
type service struct {
conf *config
repo invite.Repository
ocmClient *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.TokenExpiration == "" {
c.TokenExpiration = "24h"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
invitepb.RegisterInviteAPIServer(ss, s)
}
func getInviteRepository(c *config) (invite.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// New creates a new OCM invite manager svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
p, err := time.ParseDuration(c.TokenExpiration)
if err != nil {
return nil, err
}
c.tokenExpiration = p
repo, err := getInviteRepository(&c)
if err != nil {
return nil, err
}
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
service := &service{
conf: &c,
repo: repo,
ocmClient: client.New(&client.Config{
Timeout: time.Duration(c.OCMClientTimeout) * time.Second,
Insecure: c.OCMClientInsecure,
}),
gatewaySelector: gatewaySelector,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
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) {
user := ctxpkg.ContextMustGetUser(ctx)
token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description)
if err := s.repo.AddToken(ctx, token); err != nil {
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewInternal(ctx, "error generating invite token"),
}, nil
}
return &invitepb.GenerateInviteTokenResponse{
Status: status.NewOK(ctx),
InviteToken: token,
}, nil
}
func (s *service) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
tokens, err := s.repo.ListTokens(ctx, user.Id)
if err != nil {
return &invitepb.ListInviteTokensResponse{
Status: status.NewInternal(ctx, "error listing tokens"),
}, nil
}
return &invitepb.ListInviteTokensResponse{
Status: status.NewOK(ctx),
InviteTokens: tokens,
}, nil
}
func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider())
if err != nil {
return nil, err
}
// Accept the invitation on the remote OCM provider
remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(),
RecipientProvider: s.conf.ProviderDomain,
// The UserID is only a string here. To not lose the IDP information we use the LocalUserFederatedID encoding
// i.e. UserID@IDP
UserID: ocmuser.FormatOCMUser(user.GetId()),
Email: user.GetMail(),
Name: user.GetDisplayName(),
})
if err != nil {
switch {
case errors.Is(err, client.ErrTokenInvalid):
return &invitepb.ForwardInviteResponse{
Status: status.NewInvalid(ctx, "token not valid"),
}, nil
case errors.Is(err, client.ErrTokenNotFound):
return &invitepb.ForwardInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
case errors.Is(err, client.ErrUserAlreadyAccepted):
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &invitepb.ForwardInviteResponse{
Status: status.NewPermissionDenied(ctx, err, err.Error()),
}, nil
default:
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
// create a link between the user that accepted the share (in ctx)
// and the remote one (the initiator), so at the end of the invitation workflow they
// know each other
// remoteUser.UserID is the federated ID (just a string), to get a unique CS3 userid
// we're using the provider domain as the IDP part of the ID
remoteUserID := &userpb.UserId{
Type: userpb.UserType_USER_TYPE_FEDERATED,
Idp: req.GetOriginSystemProvider().Domain,
OpaqueId: remoteUser.UserID,
}
// we need to use a unique identifier for federated users
remoteUserID = ocmuser.LocalUserFederatedID(remoteUserID, "")
if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
Id: remoteUserID,
Mail: remoteUser.Email,
DisplayName: remoteUser.Name,
}); err != nil {
if errors.Is(err, invite.ErrUserAlreadyAccepted) {
return &invitepb.ForwardInviteResponse{
Status: status.NewAlreadyExists(ctx, err, err.Error()),
}, nil
}
return &invitepb.ForwardInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
return &invitepb.ForwardInviteResponse{
Status: status.NewOK(ctx),
UserId: remoteUserID,
Email: remoteUser.Email,
DisplayName: remoteUser.Name,
}, nil
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) {
token, err := s.repo.GetToken(ctx, req.InviteToken.Token)
if err != nil {
if errors.Is(err, invite.ErrTokenNotFound) {
return &invitepb.AcceptInviteResponse{
Status: status.NewNotFound(ctx, "token not found"),
}, nil
}
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
if !isTokenValid(token) {
return &invitepb.AcceptInviteResponse{
Status: status.NewInvalid(ctx, "token is not valid"),
}, nil
}
initiator, err := s.getUserInfo(ctx, token.UserId)
if err != nil {
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
remoteUser := req.GetRemoteUser()
// we need to use a unique identifier for federated users
remoteUser.Id = ocmuser.LocalUserFederatedID(remoteUser.Id, "")
if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), remoteUser); err != nil {
if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
// skip error if user was already accepted
return &invitepb.AcceptInviteResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
return &invitepb.AcceptInviteResponse{
Status: status.NewOK(ctx),
UserId: initiator.GetId(),
Email: initiator.Mail,
DisplayName: initiator.DisplayName,
}, nil
}
func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) {
gw, err := s.gatewaySelector.Next()
if err != nil {
return nil, nil
}
res, err := gw.GetUser(ctx, &userpb.GetUserRequest{
UserId: id,
})
if err != nil {
return nil, err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(res.Status.Message)
}
return res.User, nil
}
func isTokenValid(token *invitepb.InviteToken) bool {
return time.Now().Unix() < int64(token.Expiration.Seconds)
}
func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) {
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{
Status: status.NewInternal(ctx, "error fetching remote user details"),
}, nil
}
return &invitepb.GetAcceptedUserResponse{
Status: status.NewOK(ctx),
RemoteUser: remoteUser,
}, 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())
if err != nil {
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewInternal(ctx, "error finding remote users: "+err.Error()),
}, nil
}
return &invitepb.FindAcceptedUsersResponse{
Status: status.NewOK(ctx),
AcceptedUsers: acceptedUsers,
}, nil
}
func (s *service) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteRemoteUser(ctx, user.Id, req.RemoteUserId); err != nil {
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewInternal(ctx, "error deleting remote users: "+err.Error()),
}, nil
}
return &invitepb.DeleteAcceptedUserResponse{
Status: status.NewOK(ctx),
}, nil
}
@@ -0,0 +1,45 @@
// 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 ocminvitemanager
import (
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
)
// CreateToken creates a InviteToken object for the userID indicated by userID.
func CreateToken(expiration time.Duration, userID *userpb.UserId, description string) *invitepb.InviteToken {
tokenID := uuid.New().String()
now := time.Now()
expirationTime := now.Add(expiration)
return &invitepb.InviteToken{
Token: tokenID,
UserId: userID,
Expiration: &typesv1beta1.Timestamp{
Seconds: uint64(expirationTime.Unix()),
Nanos: 0,
},
Description: description,
}
}
@@ -0,0 +1,147 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package ocmproviderauthorizer
import (
"context"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmproviderauthorizer", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
type service struct {
conf *config
pa provider.Authorizer
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func (s *service) Register(ss *grpc.Server) {
ocmprovider.RegisterProviderAPIServer(ss, s)
}
func getProviderAuthorizer(c *config) (provider.Authorizer, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new OCM provider authorizer svc
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
pa, err := getProviderAuthorizer(c)
if err != nil {
return nil, err
}
service := &service{
conf: c,
pa: pa,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{
"/cs3.ocm.provider.v1beta1.ProviderAPI/IsProviderAllowed",
"/cs3.ocm.provider.v1beta1.ProviderAPI/ListAllProviders",
}
}
func (s *service) GetInfoByDomain(ctx context.Context, req *ocmprovider.GetInfoByDomainRequest) (*ocmprovider.GetInfoByDomainResponse, error) {
domainInfo, err := s.pa.GetInfoByDomain(ctx, req.Domain)
if err != nil {
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewInternal(ctx, "error getting provider info"),
}, nil
}
return &ocmprovider.GetInfoByDomainResponse{
Status: status.NewOK(ctx),
ProviderInfo: domainInfo,
}, nil
}
func (s *service) IsProviderAllowed(ctx context.Context, req *ocmprovider.IsProviderAllowedRequest) (*ocmprovider.IsProviderAllowedResponse, error) {
err := s.pa.IsProviderAllowed(ctx, req.Provider)
if err != nil {
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewInternal(ctx, "error verifying mesh provider"),
}, nil
}
return &ocmprovider.IsProviderAllowedResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) ListAllProviders(ctx context.Context, req *ocmprovider.ListAllProvidersRequest) (*ocmprovider.ListAllProvidersResponse, error) {
providers, err := s.pa.ListAllProviders(ctx)
if err != nil {
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewInternal(ctx, "error retrieving mesh providers"),
}, nil
}
return &ocmprovider.ListAllProvidersResponse{
Status: status.NewOK(ctx),
Providers: providers,
}, nil
}
@@ -0,0 +1,518 @@
// 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 ocmshareprovider
import (
"context"
"net/url"
"path/filepath"
"strings"
"text/template"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/ocmd"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/ocm/client"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share/repository/registry"
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("ocmshareprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
ClientTimeout int `mapstructure:"client_timeout"`
ClientInsecure bool `mapstructure:"client_insecure"`
GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"`
WebDAVEndpoint string `mapstructure:"webdav_endpoint" validate:"required"`
WebappTemplate string `mapstructure:"webapp_template"`
}
type service struct {
conf *config
repo share.Repository
client *client.OCMClient
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
webappTmpl *template.Template
walker walker.Walker
}
func (c *config) ApplyDefaults() {
if c.Driver == "" {
c.Driver = "json"
}
if c.ClientTimeout == 0 {
c.ClientTimeout = 10
}
if c.WebappTemplate == "" {
c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}"
}
c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC)
}
func (s *service) Register(ss *grpc.Server) {
ocm.RegisterOcmAPIServer(ss, s)
}
func getShareRepository(c *config) (share.Repository, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// New creates a new ocm share provider svc.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
repo, err := getShareRepository(&c)
if err != nil {
return nil, err
}
client := client.New(&client.Config{
Timeout: time.Duration(c.ClientTimeout) * time.Second,
Insecure: c.ClientInsecure,
})
gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC)
if err != nil {
return nil, err
}
tpl, err := template.New("webapp_template").Parse(c.WebappTemplate)
if err != nil {
return nil, err
}
walker := walker.NewWalker(gatewaySelector)
service := &service{
conf: &c,
repo: repo,
client: client,
gatewaySelector: gatewaySelector,
webappTmpl: tpl,
walker: walker,
}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"}
}
func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) {
for _, s := range originProvider.Services {
if s.Endpoint.Type.Name == "OCM" {
return s.Endpoint.Path, nil
}
}
return "", errors.New("ocm endpoint not specified for mesh provider")
}
func getResourceType(info *providerpb.ResourceInfo) string {
switch info.Type {
case providerpb.ResourceType_RESOURCE_TYPE_FILE:
return "file"
case providerpb.ResourceType_RESOURCE_TYPE_CONTAINER:
return "folder"
}
return "unknown"
}
func (s *service) webdavURL(_ 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, "/dav/ocm", share.GetId().GetOpaqueId())
return p
}
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")
}
if m.WebdavOptions.Permissions.InitiateFileUpload {
perms = append(perms, "write")
}
return &ocmd.WebDAV{
Permissions: perms,
URI: s.webdavURL(ctx, share),
SharedSecret: share.Token,
}
}
func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp {
var b strings.Builder
if err := s.webappTmpl.Execute(&b, share); err != nil {
return nil
}
return &ocmd.Webapp{
URITemplate: b.String(),
}
}
func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols {
var p ocmd.Protocols
for _, m := range share.AccessMethods {
var newProtocol ocmd.Protocol
switch t := m.Term.(type) {
case *ocm.AccessMethod_WebdavOptions:
newProtocol = s.getWebdavProtocol(ctx, share, t)
case *ocm.AccessMethod_WebappOptions:
newProtocol = s.getWebappProtocol(share)
}
if newProtocol != nil {
p = append(p, newProtocol)
}
}
return p
}
func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{
Ref: &providerpb.Reference{
ResourceId: req.ResourceId,
},
})
if err != nil {
return nil, err
}
if statRes.Status.Code != rpc.Code_CODE_OK {
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
return &ocm.CreateOCMShareResponse{
Status: status.NewNotFound(ctx, statRes.Status.Message),
}, nil
}
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, statRes.Status.Message),
}, nil
}
info := statRes.Info
user := ctxpkg.ContextMustGetUser(ctx)
tkn := utils.RandString(32)
now := time.Now().UnixNano()
ts := &typespb.Timestamp{
Seconds: uint64(now / 1000000000),
Nanos: uint32(now % 1000000000),
}
// 1. persist the share in the repository
ocmshare := &ocm.Share{
Token: tkn,
Name: filepath.Base(info.Path),
ResourceId: req.ResourceId,
Grantee: req.Grantee,
ShareType: ocm.ShareType_SHARE_TYPE_USER,
Owner: info.Owner,
Creator: user.Id,
Ctime: ts,
Mtime: ts,
Expiration: req.Expiration,
AccessMethods: req.AccessMethods,
}
ocmshare, err = s.repo.StoreShare(ctx, ocmshare)
if err != nil {
if errors.Is(err, share.ErrShareAlreadyExisting) {
return &ocm.CreateOCMShareResponse{
Status: status.NewAlreadyExists(ctx, err, "share already exists"),
}, nil
}
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
// 2. create the share on the remote provider
// 2.a get the ocm endpoint of the remote provider
ocmEndpoint, err := getOCMEndpoint(req.RecipientMeshProvider)
if err != nil {
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, "the selected provider does not have an OCM endpoint"),
}, nil
}
// 2.b replace outgoing user ids with ocm user ids
// unpack the federated user id
shareWith := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(req.GetGrantee().GetUserId(), ""))
// wrap the local user id in a local federated user id
owner := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(info.Owner, s.conf.ProviderDomain))
sender := ocmuser.FormatOCMUser(ocmuser.LocalUserFederatedID(user.Id, s.conf.ProviderDomain))
newShareReq := &client.NewShareRequest{
ShareWith: shareWith,
Name: ocmshare.Name,
ProviderID: ocmshare.Id.OpaqueId,
Owner: owner,
Sender: sender,
SenderDisplayName: user.DisplayName,
ShareType: "user",
ResourceType: getResourceType(info),
Protocols: s.getProtocols(ctx, ocmshare),
}
if req.Expiration != nil {
newShareReq.Expiration = req.Expiration.Seconds
}
// 2.c make POST /shares request
newShareRes, err := s.client.NewShare(ctx, ocmEndpoint, newShareReq)
if err != nil {
err2 := s.repo.DeleteShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: ocmshare.Id}})
if err2 != nil {
appctx.GetLogger(ctx).Error().Err(err2).Str("shareid", ocmshare.GetId().GetOpaqueId()).Msg("could not delete local ocm share")
}
// TODO remove the share from the local storage
switch {
case errors.Is(err, client.ErrInvalidParameters):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
case errors.Is(err, client.ErrServiceNotTrusted):
return &ocm.CreateOCMShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
default:
return &ocm.CreateOCMShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
res := &ocm.CreateOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
RecipientDisplayName: newShareRes.RecipientDisplayName,
}
return res, nil
}
func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
// TODO (gdelmont): notify the remote provider using the /notification ocm endpoint
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
user := ctxpkg.ContextMustGetUser(ctx)
if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.RemoveOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error removing share"),
}, nil
}
return &ocm.RemoveOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) {
// 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) {
return &ocm.GetOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetOCMShareResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
return &ocm.GetOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}, 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, "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)
if err != nil {
return &ocm.ListOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing shares"),
}, nil
}
res := &ocm.ListOCMSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if len(req.Field) == 0 {
return &ocm.UpdateOCMShareResponse{
Status: status.NewOK(ctx),
}, nil
}
_, err := s.repo.UpdateShare(ctx, user, req.Ref, req.Field...)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateOCMShareResponse{
Status: status.NewInternal(ctx, "error updating share"),
}, nil
}
res := &ocm.UpdateOCMShareResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
shares, err := s.repo.ListReceivedShares(ctx, user)
if err != nil {
return &ocm.ListReceivedOCMSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
}, nil
}
res := &ocm.ListReceivedOCMSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
_, err := s.repo.UpdateReceivedShare(ctx, user, req.Share, req.UpdateMask)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error updating received share"),
}, nil
}
res := &ocm.UpdateReceivedOCMShareResponse{
Status: status.NewOK(ctx),
}
return res, nil
}
func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) {
user := ctxpkg.ContextMustGetUser(ctx)
if user.Id.GetType() == userpb.UserType_USER_TYPE_SERVICE {
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
user = &userpb.User{
Id: &uid,
}
}
ocmshare, err := s.repo.GetReceivedShare(ctx, user, req.Ref)
if err != nil {
if errors.Is(err, share.ErrShareNotFound) {
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewNotFound(ctx, "share does not exist"),
}, nil
}
return &ocm.GetReceivedOCMShareResponse{
Status: status.NewInternal(ctx, "error getting received share: "+err.Error()),
}, nil
}
res := &ocm.GetReceivedOCMShareResponse{
Status: status.NewOK(ctx),
Share: ocmshare,
}
return res, nil
}
@@ -0,0 +1,105 @@
// Copyright 2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package permissions
import (
"context"
"fmt"
permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/permission/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("permissions", New)
}
type config struct {
Driver string `mapstructure:"driver" docs:"localhome;The permission driver to be used."`
Drivers map[string]map[string]interface{} `mapstructure:"drivers" docs:"url:pkg/permission/permission.go"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
type service struct {
manager permission.Manager
}
// New returns a new PermissionsServiceServer
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
f, ok := registry.NewFuncs[c.Driver]
if !ok {
return nil, fmt.Errorf("could not get permission manager '%s'", c.Driver)
}
manager, err := f(c.Drivers[c.Driver])
if err != nil {
return nil, err
}
service := &service{manager: manager}
return service, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
permissions.RegisterPermissionsAPIServer(ss, s)
}
func (s *service) CheckPermission(ctx context.Context, req *permissions.CheckPermissionRequest) (*permissions.CheckPermissionResponse, error) {
var subject string
switch ref := req.SubjectRef.Spec.(type) {
case *permissions.SubjectReference_UserId:
subject = ref.UserId.OpaqueId
case *permissions.SubjectReference_GroupId:
subject = ref.GroupId.OpaqueId
}
var status *rpc.Status
if ok := s.manager.CheckPermission(req.Permission, subject, req.Ref); ok {
status = &rpc.Status{Code: rpc.Code_CODE_OK}
} else {
status = &rpc.Status{Code: rpc.Code_CODE_PERMISSION_DENIED}
}
return &permissions.CheckPermissionResponse{Status: status}, nil
}
@@ -0,0 +1,134 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package preferences
import (
"context"
"google.golang.org/grpc"
preferencespb "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/preferences"
"github.com/opencloud-eu/reva/v2/pkg/preferences/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
func init() {
rgrpc.Register("preferences", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "memory"
}
}
type service struct {
conf *config
pm preferences.Manager
}
func getPreferencesManager(c *config) (preferences.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns a new PreferencesServiceServer
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
pm, err := getPreferencesManager(c)
if err != nil {
return nil, err
}
return &service{
conf: c,
pm: pm,
}, nil
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
preferencespb.RegisterPreferencesAPIServer(ss, s)
}
func (s *service) SetKey(ctx context.Context, req *preferencespb.SetKeyRequest) (*preferencespb.SetKeyResponse, error) {
err := s.pm.SetKey(ctx, req.Key.Key, req.Key.Namespace, req.Val)
if err != nil {
return &preferencespb.SetKeyResponse{
Status: status.NewInternal(ctx, "error setting key"),
}, nil
}
return &preferencespb.SetKeyResponse{
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetKey(ctx context.Context, req *preferencespb.GetKeyRequest) (*preferencespb.GetKeyResponse, error) {
val, err := s.pm.GetKey(ctx, req.Key.Key, req.Key.Namespace)
if err != nil {
st := status.NewInternal(ctx, "error retrieving key")
if _, ok := err.(errtypes.IsNotFound); ok {
st = status.NewNotFound(ctx, "key not found")
}
return &preferencespb.GetKeyResponse{
Status: st,
}, nil
}
return &preferencespb.GetKeyResponse{
Status: status.NewOK(ctx),
Val: val,
}, nil
}
@@ -0,0 +1,654 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package publicshareprovider
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/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/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/password"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
)
const getUserCtxErrMsg = "error getting user from context"
func init() {
rgrpc.Register("publicshareprovider", NewDefault)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
GatewayAddr string `mapstructure:"gateway_addr"`
AllowedPathsForShares []string `mapstructure:"allowed_paths_for_shares"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
WriteableShareMustHavePassword bool `mapstructure:"writeable_share_must_have_password"`
PublicShareMustHavePassword bool `mapstructure:"public_share_must_have_password"`
PasswordPolicy map[string]interface{} `mapstructure:"password_policy"`
}
type passwordPolicy struct {
MinCharacters int `mapstructure:"min_characters"`
MinLowerCaseCharacters int `mapstructure:"min_lowercase_characters"`
MinUpperCaseCharacters int `mapstructure:"min_uppercase_characters"`
MinDigits int `mapstructure:"min_digits"`
MinSpecialCharacters int `mapstructure:"min_special_characters"`
BannedPasswordsList map[string]struct{} `mapstructure:"banned_passwords_list"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
type service struct {
conf *config
sm publicshare.Manager
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
allowedPathsForShares []*regexp.Regexp
passwordValidator password.Validator
}
func getShareManager(c *config) (publicshare.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// TODO(labkode): add ctx to Close.
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.sharing.link.v1beta1.LinkAPI/GetPublicShareByToken"}
}
func (s *service) Register(ss *grpc.Server) {
link.RegisterLinkAPIServer(ss, s)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding config")
return nil, err
}
return c, nil
}
func parsePasswordPolicy(m map[string]interface{}) (*passwordPolicy, error) {
p := &passwordPolicy{}
if err := mapstructure.Decode(m, p); err != nil {
err = errors.Wrap(err, "error decoding password policy config")
return nil, err
}
return p, nil
}
// New creates a new public share provider svc initialized from defaults
func NewDefault(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
p, err := parsePasswordPolicy(c.PasswordPolicy)
if err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c)
if err != nil {
return nil, err
}
gatewaySelector, err := pool.GatewaySelector(sharedconf.GetGatewaySVC(c.GatewayAddr))
if err != nil {
return nil, err
}
return New(gatewaySelector, sm, c, p)
}
// New creates a new user share provider svc
func New(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], sm publicshare.Manager, c *config, p *passwordPolicy) (rgrpc.Service, error) {
allowedPathsForShares := make([]*regexp.Regexp, 0, len(c.AllowedPathsForShares))
for _, s := range c.AllowedPathsForShares {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedPathsForShares = append(allowedPathsForShares, regex)
}
service := &service{
conf: c,
sm: sm,
gatewaySelector: gatewaySelector,
allowedPathsForShares: allowedPathsForShares,
passwordValidator: newPasswordPolicy(p),
}
return service, nil
}
func newPasswordPolicy(c *passwordPolicy) password.Validator {
if c == nil {
return password.NewPasswordPolicy(0, 0, 0, 0, 0, nil)
}
return password.NewPasswordPolicy(
c.MinCharacters,
c.MinLowerCaseCharacters,
c.MinUpperCaseCharacters,
c.MinDigits,
c.MinSpecialCharacters,
c.BannedPasswordsList,
)
}
func (s *service) isPathAllowed(path string) bool {
if len(s.allowedPathsForShares) == 0 {
return true
}
for _, reg := range s.allowedPathsForShares {
if reg.MatchString(path) {
return true
}
}
return false
}
func (s *service) CreatePublicShare(ctx context.Context, req *link.CreatePublicShareRequest) (*link.CreatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "create").Msg("create public share")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
isInternalLink := grants.PermissionsEqual(req.GetGrant().GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: req.GetResourceInfo().GetId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetResourceInfo().GetId()).Msg("failed to stat resource to share")
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat resource to share"),
}, err
}
// all users can create internal links
if !isInternalLink {
// check if the user has the permission in the user role
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, gatewayClient)
if err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write public link"),
}, err
}
if !ok {
return &link.CreatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create public links"),
}, nil
}
}
// check that user has share permissions
if !isInternalLink && !sRes.GetInfo().GetPermissionSet().AddGrant {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "no share permission"),
}, nil
}
// check if the user can share with the desired permissions. For internal links this is skipped,
// users can always create internal links provided they have the AddGrant permission, which was already
// checked above
if !isInternalLink && !conversions.SufficientCS3Permissions(sRes.GetInfo().GetPermissionSet(), req.GetGrant().GetPermissions().GetPermissions()) {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "insufficient permissions to create that kind of share"),
}, nil
}
// validate path
if !s.isPathAllowed(req.GetResourceInfo().GetPath()) {
return &link.CreatePublicShareResponse{
Status: status.NewFailedPrecondition(ctx, nil, "share creation is not allowed for the specified path"),
}, nil
}
// check that this is a not a personal space root
if req.GetResourceInfo().GetId().GetOpaqueId() == req.GetResourceInfo().GetId().GetSpaceId() &&
req.GetResourceInfo().GetSpace().GetSpaceType() == "personal" {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "cannot create link on personal space root"),
}, nil
}
// quick link returns the existing one if already present
quickLink, err := checkQuicklink(req.GetResourceInfo())
if err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "invalid quicklink value"),
}, nil
}
if quickLink {
f := []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(req.GetResourceInfo().GetId())}
req := link.ListPublicSharesRequest{Filters: f}
res, err := s.ListPublicShares(ctx, &req)
if err != nil || res.GetStatus().GetCode() != rpc.Code_CODE_OK {
return &link.CreatePublicShareResponse{
Status: status.NewInternal(ctx, "could not list public links"),
}, nil
}
for _, l := range res.GetShare() {
if l.Quicklink {
return &link.CreatePublicShareResponse{
Status: status.NewOK(ctx),
Share: l,
}, nil
}
}
}
grant := req.GetGrant()
// validate expiration date
if grant.GetExpiration() != nil {
expirationDateTime := utils.TSToTime(grant.GetExpiration()).UTC()
if expirationDateTime.Before(time.Now().UTC()) {
msg := fmt.Sprintf("expiration date is in the past: %s", expirationDateTime.Format(time.RFC3339))
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, msg),
}, nil
}
}
// enforce password if needed
setPassword := grant.GetPassword()
if !isInternalLink && enforcePassword(false, grant.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// validate password policy
if len(setPassword) > 0 {
if err := s.passwordValidator.Validate(setPassword); err != nil {
return &link.CreatePublicShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
}
}
user := ctxpkg.ContextMustGetUser(ctx)
res := &link.CreatePublicShareResponse{}
share, err := s.sm.CreatePublicShare(ctx, user, req.GetResourceInfo(), req.GetGrant())
switch {
case err != nil:
log.Error().Err(err).Interface("request", req).Msg("could not write public share")
res.Status = status.NewInternal(ctx, "error persisting public share:"+err.Error())
default:
res.Status = status.NewOK(ctx)
res.Share = share
res.Opaque = utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName())
}
return res, nil
}
func (s *service) RemovePublicShare(ctx context.Context, req *link.RemovePublicShareRequest) (*link.RemovePublicShareResponse, error) {
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "remove").Msg("remove public share")
user := ctxpkg.ContextMustGetUser(ctx)
ps, err := s.sm.GetPublicShare(ctx, user, req.GetRef(), false)
if err != nil {
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "error loading public share"),
}, err
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ps.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", ps.ResourceId).Msg("failed to stat shared resource")
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
if !publicshare.IsCreatedByUser(ps, user) {
if !sRes.GetInfo().GetPermissionSet().RemoveGrant {
return &link.RemovePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to delete public share"),
}, err
}
}
err = s.sm.RevokePublicShare(ctx, user, req.Ref)
if err != nil {
return &link.RemovePublicShareResponse{
Status: status.NewInternal(ctx, "error deleting public share"),
}, err
}
o := utils.AppendJSONToOpaque(nil, "resourceid", ps.GetResourceId())
o = utils.AppendPlainToOpaque(o, "resourcename", sRes.GetInfo().GetName())
return &link.RemovePublicShareResponse{
Opaque: o,
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublicShareByTokenRequest) (*link.GetPublicShareByTokenResponse, error) {
log := appctx.GetLogger(ctx)
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.GetAuthentication(), req.GetSign())
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, "unexpected error"),
}, nil
}
}
func (s *service) GetPublicShare(ctx context.Context, req *link.GetPublicShareRequest) (*link.GetPublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "get").Msg("get public share")
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
log.Error().Msg(getUserCtxErrMsg)
}
ps, err := s.sm.GetPublicShare(ctx, u, req.Ref, req.GetSign())
switch {
case err != nil:
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &link.GetPublicShareResponse{
Status: st,
}, nil
case ps == nil:
return &link.GetPublicShareResponse{
Status: status.NewNotFound(ctx, "not found"),
}, nil
default:
return &link.GetPublicShareResponse{
Status: status.NewOK(ctx),
Share: ps,
}, nil
}
}
func (s *service) ListPublicShares(ctx context.Context, req *link.ListPublicSharesRequest) (*link.ListPublicSharesResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "list").Msg("list public share")
user, _ := ctxpkg.ContextGetUser(ctx)
shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, req.GetSign())
if err != nil {
log.Err(err).Msg("error listing shares")
return &link.ListPublicSharesResponse{
Status: status.NewInternal(ctx, "error listing public shares"),
}, nil
}
res := &link.ListPublicSharesResponse{
Status: status.NewOK(ctx),
Share: shares,
}
return res, nil
}
func (s *service) UpdatePublicShare(ctx context.Context, req *link.UpdatePublicShareRequest) (*link.UpdatePublicShareResponse, error) {
log := appctx.GetLogger(ctx)
log.Info().Str("publicshareprovider", "update").Msg("update public share")
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
user := ctxpkg.ContextMustGetUser(ctx)
ps, err := s.sm.GetPublicShare(ctx, user, req.GetRef(), false)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "error loading public share"),
}, err
}
isInternalLink := isInternalLink(req, ps)
// check if the user has the permission in the user role
if !publicshare.IsCreatedByUser(ps, user) {
canWriteLink, err := utils.CheckPermission(ctx, permission.WritePublicLink, gatewayClient)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "error checking permission to write public share"),
}, err
}
if !canWriteLink {
return &link.UpdatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to update public share"),
}, nil
}
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: ps.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", ps.ResourceId).Msg("failed to stat shared resource")
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
if sRes.Status.Code != rpc.Code_CODE_OK {
return &link.UpdatePublicShareResponse{
Status: sRes.GetStatus(),
}, nil
}
if !isInternalLink && !publicshare.IsCreatedByUser(ps, user) {
if !sRes.GetInfo().GetPermissionSet().UpdateGrant {
return &link.UpdatePublicShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to update public share"),
}, err
}
}
// check if the user can change the permissions to the desired permissions
updatePermissions := req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS
if updatePermissions &&
!isInternalLink &&
!conversions.SufficientCS3Permissions(
sRes.GetInfo().GetPermissionSet(),
req.GetUpdate().GetGrant().GetPermissions().GetPermissions(),
) {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "insufficient permissions to update that kind of share"),
}, nil
}
if updatePermissions {
beforePerm, _ := json.Marshal(sRes.GetInfo().GetPermissionSet())
afterPerm, _ := json.Marshal(req.GetUpdate().GetGrant().GetPermissions())
log.Info().
Str("shares", "update").
Msgf("updating permissions from %v to: %v",
string(beforePerm),
string(afterPerm),
)
}
grant := req.GetUpdate().GetGrant()
// validate expiration date
if grant.GetExpiration() != nil {
expirationDateTime := utils.TSToTime(grant.GetExpiration()).UTC()
if expirationDateTime.Before(time.Now().UTC()) {
msg := fmt.Sprintf("expiration date is in the past: %s", expirationDateTime.Format(time.RFC3339))
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, msg),
}, nil
}
}
// enforce password if needed
var canOptOut bool
if !isInternalLink {
canOptOut, err = utils.CheckPermission(ctx, permission.DeleteReadOnlyPassword, gatewayClient)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
updatePassword := req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PASSWORD
setPassword := grant.GetPassword()
// we update permissions with an empty password and password is not set on the public share
emptyPasswordInPermissionUpdate := len(setPassword) == 0 && updatePermissions && !ps.PasswordProtected
// password is updated, we use the current permissions to check if the user can opt out
if updatePassword && !isInternalLink && enforcePassword(canOptOut, ps.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// permissions are updated, we use the new permissions to check if the user can opt out
if emptyPasswordInPermissionUpdate && !isInternalLink && enforcePassword(canOptOut, grant.GetPermissions().GetPermissions(), s.conf) && len(setPassword) == 0 {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, "password protection is enforced"),
}, nil
}
// validate password policy
if updatePassword && len(setPassword) > 0 {
if err := s.passwordValidator.Validate(setPassword); err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInvalidArg(ctx, err.Error()),
}, nil
}
}
updateR, err := s.sm.UpdatePublicShare(ctx, user, req)
if err != nil {
return &link.UpdatePublicShareResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
res := &link.UpdatePublicShareResponse{
Status: status.NewOK(ctx),
Share: updateR,
Opaque: utils.AppendPlainToOpaque(nil, "resourcename", sRes.GetInfo().GetName()),
}
return res, nil
}
func isInternalLink(req *link.UpdatePublicShareRequest, ps *link.PublicShare) bool {
switch {
case req.GetUpdate().GetType() == link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
return grants.PermissionsEqual(req.GetUpdate().GetGrant().GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
default:
return grants.PermissionsEqual(ps.GetPermissions().GetPermissions(), &provider.ResourcePermissions{})
}
}
func enforcePassword(canOptOut bool, permissions *provider.ResourcePermissions, conf *config) bool {
isReadOnly := conversions.SufficientCS3Permissions(conversions.NewViewerRole().CS3ResourcePermissions(), permissions)
if isReadOnly && canOptOut {
return false
}
if conf.PublicShareMustHavePassword {
return true
}
return !isReadOnly && conf.WriteableShareMustHavePassword
}
func checkQuicklink(info *provider.ResourceInfo) (bool, error) {
if info == nil {
return false, nil
}
if m := info.GetArbitraryMetadata().GetMetadata(); m != nil {
q, ok := m["quicklink"]
// empty string would trigger an error in ParseBool()
if !ok || q == "" {
return false, nil
}
quickLink, err := strconv.ParseBool(q)
if err != nil {
return false, err
}
return quickLink, nil
}
return false, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package storageprovider
import (
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// XS defines an hex-encoded string as checksum.
type XS string
func (x XS) String() string { return string(x) }
const (
// XSInvalid means the checksum type is invalid.
XSInvalid XS = "invalid"
// XSUnset means the checksum is optional.
XSUnset = "unset"
// XSAdler32 means the checksum is adler32
XSAdler32 = "adler32"
// XSMD5 means the checksum is md5
XSMD5 = "md5"
// XSSHA1 means the checksum is SHA1
XSSHA1 = "sha1"
// XSSHA256 means the checksum is SHA256.
XSSHA256 = "sha256"
)
// GRPC2PKGXS converts the grpc checksum type to an internal pkg type.
func GRPC2PKGXS(t provider.ResourceChecksumType) XS {
switch t {
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID:
return XSInvalid
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_UNSET:
return XSUnset
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_SHA1:
return XSSHA1
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_ADLER32:
return XSAdler32
case provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_MD5:
return XSMD5
default:
return XSInvalid
}
}
// PKG2GRPCXS converts an internal checksum type to the grpc checksum type.
func PKG2GRPCXS(xsType string) provider.ResourceChecksumType {
switch xsType {
case XSUnset:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_UNSET
case XSAdler32:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_ADLER32
case XSMD5:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_MD5
case XSSHA1:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_SHA1
default:
return provider.ResourceChecksumType_RESOURCE_CHECKSUM_TYPE_INVALID
}
}
@@ -0,0 +1,177 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package storageregistry
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
"github.com/BurntSushi/toml"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registrypb "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/registry/registry"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
func init() {
rgrpc.Register("storageregistry", New)
}
type service struct {
reg storage.Registry
}
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
registrypb.RegisterRegistryAPIServer(ss, s)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "static"
}
}
// New creates a new StorageBrokerService
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
reg, err := getRegistry(c)
if err != nil {
return nil, err
}
service := &service{
reg: reg,
}
return service, nil
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
func getRegistry(c *config) (storage.Registry, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
func (s *service) ListStorageProviders(ctx context.Context, req *registrypb.ListStorageProvidersRequest) (*registrypb.ListStorageProvidersResponse, error) {
pinfos, err := s.reg.ListProviders(ctx, sdk.DecodeOpaqueMap(req.Opaque))
if err != nil {
return &registrypb.ListStorageProvidersResponse{
Status: status.NewInternal(ctx, "error getting list of storage providers"),
}, nil
}
res := &registrypb.ListStorageProvidersResponse{
Status: status.NewOK(ctx),
Providers: pinfos,
}
return res, nil
}
func (s *service) GetStorageProviders(ctx context.Context, req *registrypb.GetStorageProvidersRequest) (*registrypb.GetStorageProvidersResponse, error) {
space, err := decodeSpace(req.Opaque)
if err != nil {
return &registrypb.GetStorageProvidersResponse{
Status: status.NewInvalid(ctx, err.Error()),
}, nil
}
p, err := s.reg.GetProvider(ctx, space)
if err != nil {
switch err.(type) {
case errtypes.IsNotFound:
return &registrypb.GetStorageProvidersResponse{
Status: status.NewNotFound(ctx, err.Error()),
}, nil
default:
return &registrypb.GetStorageProvidersResponse{
Status: status.NewInternal(ctx, "error finding storage provider"),
}, nil
}
}
res := &registrypb.GetStorageProvidersResponse{
Status: status.NewOK(ctx),
Providers: []*registrypb.ProviderInfo{p},
}
return res, nil
}
func decodeSpace(o *typespb.Opaque) (*provider.StorageSpace, error) {
if entry, ok := o.Map["space"]; ok {
var unmarshal func([]byte, interface{}) error
switch entry.Decoder {
case "json":
unmarshal = json.Unmarshal
case "toml":
unmarshal = toml.Unmarshal
case "xml":
unmarshal = xml.Unmarshal
}
space := &provider.StorageSpace{}
return space, unmarshal(entry.Value, space)
}
return nil, fmt.Errorf("missing space in opaque property")
}
func (s *service) GetHome(ctx context.Context, req *registrypb.GetHomeRequest) (*registrypb.GetHomeResponse, error) {
res := &registrypb.GetHomeResponse{
Status: status.NewUnimplemented(ctx, nil, "getHome is no longer used. use List"),
}
return res, nil
}
@@ -0,0 +1,316 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package userprovider
import (
"context"
"fmt"
"path/filepath"
"sort"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
tenantpb "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/plugin"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/tenant"
tenantRegistry "github.com/opencloud-eu/reva/v2/pkg/tenant/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/user"
userRegistry "github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
)
func init() {
rgrpc.Register("userprovider", New)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
TenantDriver string `mapstructure:"tenant_driver"`
TenantDrivers map[string]map[string]interface{} `mapstructure:"tenant_drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
// Fall back to user driver/drivers when no tenant-specific config is provided.
if c.TenantDriver == "" {
c.TenantDriver = c.Driver
}
if c.TenantDrivers == nil {
c.TenantDrivers = c.Drivers
}
// Force "null" driver if multi-tenancy is disabled
if !sharedconf.MultiTenantEnabled() {
c.TenantDriver = "null"
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
c.init()
return c, nil
}
func getDriver(c *config) (user.Manager, *plugin.RevaPlugin, error) {
p, err := plugin.Load("userprovider", c.Driver)
if err == nil {
manager, ok := p.Plugin.(user.Manager)
if !ok {
return nil, nil, fmt.Errorf("could not assert the loaded plugin")
}
pluginConfig := filepath.Base(c.Driver)
err = manager.Configure(c.Drivers[pluginConfig])
if err != nil {
return nil, nil, err
}
return manager, p, nil
} else if _, ok := err.(errtypes.NotFound); ok {
// plugin not found, fetch the driver from the in-memory registry
if f, ok := userRegistry.NewFuncs[c.Driver]; ok {
mgr, err := f(c.Drivers[c.Driver])
return mgr, nil, err
}
} else {
return nil, nil, err
}
return nil, nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for user manager", c.Driver))
}
func getTenantManager(c *config) (tenant.Manager, error) {
if f, ok := tenantRegistry.NewFuncs[c.TenantDriver]; ok {
mgr, err := f(c.TenantDrivers[c.TenantDriver])
return mgr, err
}
return nil, errtypes.NotFound(fmt.Sprintf("driver %s not found for tenant manager", c.TenantDriver))
}
// New returns a new UserProviderServiceServer.
func New(m map[string]interface{}, ss *grpc.Server, _ *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
userManager, plug, err := getDriver(c)
if err != nil {
return nil, err
}
tenantManager, err := getTenantManager(c)
if err != nil {
return nil, err
}
return NewWithManagers(userManager, tenantManager, plug), nil
}
// NewWithManagers returns a new UserProviderService with the given managers.
func NewWithManagers(um user.Manager, tm tenant.Manager, plug *plugin.RevaPlugin) rgrpc.Service {
return &service{
usermgr: um,
tenantmgr: tm,
plugin: plug,
}
}
type service struct {
usermgr user.Manager
tenantmgr tenant.Manager
plugin *plugin.RevaPlugin
}
func (s *service) Close() error {
if s.plugin != nil {
s.plugin.Kill()
}
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.identity.user.v1beta1.UserAPI/GetUser", "/cs3.identity.user.v1beta1.UserAPI/GetUserByClaim", "/cs3.identity.user.v1beta1.UserAPI/GetUserGroups"}
}
func (s *service) Register(ss *grpc.Server) {
userpb.RegisterUserAPIServer(ss, s)
tenantpb.RegisterTenantAPIServer(ss, s)
}
func (s *service) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
if req.UserId == nil {
res := &userpb.GetUserResponse{
Status: status.NewInvalid(ctx, "userid missing"),
}
return res, nil
}
// Only request users from the same tenant as the current user
if currentUser, ok := revactx.ContextGetUser(ctx); ok {
req.UserId.TenantId = currentUser.GetId().GetTenantId()
}
user, err := s.usermgr.GetUser(ctx, req.UserId, req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.GetUserResponse{}
switch err.(type) {
case errtypes.NotFound:
res.Status = status.NewNotFound(ctx, "user not found")
case errtypes.Unavailable:
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
default:
res.Status = status.NewInternal(ctx, "error getting user")
}
return res, nil
}
res := &userpb.GetUserResponse{
Status: status.NewOK(ctx),
User: user,
}
return res, nil
}
func (s *service) GetUserByClaim(ctx context.Context, req *userpb.GetUserByClaimRequest) (*userpb.GetUserByClaimResponse, error) {
tenantID := ""
if currentUser, ok := revactx.ContextGetUser(ctx); ok {
tenantID = currentUser.GetId().GetTenantId()
}
user, err := s.usermgr.GetUserByClaim(ctx, req.Claim, req.Value, tenantID, req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.GetUserByClaimResponse{}
switch err.(type) {
case errtypes.NotFound:
res.Status = status.NewNotFound(ctx, fmt.Sprintf("user not found %s %s", req.Claim, req.Value))
case errtypes.Unavailable:
res.Status = status.NewUnavailable(ctx, "user provider temporarily unavailable")
default:
res.Status = status.NewInternal(ctx, "error getting user by claim")
}
return res, nil
}
res := &userpb.GetUserByClaimResponse{
Status: status.NewOK(ctx),
User: user,
}
return res, nil
}
func (s *service) FindUsers(ctx context.Context, req *userpb.FindUsersRequest) (*userpb.FindUsersResponse, error) {
if len(req.Filters) > 1 || req.Filters[0].GetType() != userpb.Filter_TYPE_QUERY {
return nil, fmt.Errorf("only one query filter supported")
}
currentUser := revactx.ContextMustGetUser(ctx)
users, err := s.usermgr.FindUsers(ctx, req.Filters[0].GetQuery(), currentUser.GetId().GetTenantId(), req.SkipFetchingUserGroups)
if err != nil {
res := &userpb.FindUsersResponse{
Status: status.NewInternal(ctx, "error finding users"),
}
return res, nil
}
// sort users by username
sort.Slice(users, func(i, j int) bool {
return users[i].Username <= users[j].Username
})
res := &userpb.FindUsersResponse{
Status: status.NewOK(ctx),
Users: users,
}
return res, nil
}
func (s *service) GetUserGroups(ctx context.Context, req *userpb.GetUserGroupsRequest) (*userpb.GetUserGroupsResponse, error) {
log := appctx.GetLogger(ctx)
if req.UserId == nil {
res := &userpb.GetUserGroupsResponse{
Status: status.NewInvalid(ctx, "userid missing"),
}
return res, nil
}
groups, err := s.usermgr.GetUserGroups(ctx, req.UserId)
if err != nil {
log.Warn().Err(err).Interface("userid", req.UserId).Msg("error getting user groups")
res := &userpb.GetUserGroupsResponse{
Status: status.NewInternal(ctx, "error getting user groups"),
}
return res, nil
}
res := &userpb.GetUserGroupsResponse{
Status: status.NewOK(ctx),
Groups: groups,
}
return res, nil
}
func (s *service) GetTenant(ctx context.Context, req *tenantpb.GetTenantRequest) (*tenantpb.GetTenantResponse, error) {
log := appctx.GetLogger(ctx)
t, err := s.tenantmgr.GetTenant(ctx, req.GetTenantId())
if err != nil {
log.Warn().Err(err).Interface("tenantid", req.GetTenantId()).Msg("error getting tenant")
res := &tenantpb.GetTenantResponse{
Status: status.NewInternal(ctx, "error getting tenant"),
}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "tenant not found")
}
return res, nil
}
return &tenantpb.GetTenantResponse{
Status: status.NewOK(ctx),
Tenant: t,
}, nil
}
func (s *service) GetTenantByClaim(ctx context.Context, req *tenantpb.GetTenantByClaimRequest) (*tenantpb.GetTenantByClaimResponse, error) {
log := appctx.GetLogger(ctx)
t, err := s.tenantmgr.GetTenantByClaim(ctx, req.GetClaim(), req.GetValue())
if err != nil {
log.Warn().Err(err).Interface("claim", req.GetClaim()).Interface("value", req.GetValue()).Msg("error getting tenant")
res := &tenantpb.GetTenantByClaimResponse{
Status: status.NewInternal(ctx, "error getting tenant"),
}
if _, ok := err.(errtypes.NotFound); ok {
res.Status = status.NewNotFound(ctx, "tenant not found")
}
return res, nil
}
return &tenantpb.GetTenantByClaimResponse{
Status: status.NewOK(ctx),
Tenant: t,
}, nil
}
@@ -0,0 +1,857 @@
// Copyright 2018-2021 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package usershareprovider
import (
"context"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
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"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
const (
_fieldMaskPathMountPoint = "mount_point"
_fieldMaskPathPermissions = "permissions"
_fieldMaskPathState = "state"
_spaceTypePersonal = "personal"
_spaceTypeProject = "project"
_spaceTypeVirtual = "virtual"
)
func init() {
rgrpc.Register("usershareprovider", NewDefault)
}
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
GatewayAddr string `mapstructure:"gateway_addr"`
AllowedPathsForShares []string `mapstructure:"allowed_paths_for_shares"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
type service struct {
sm share.Manager
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
allowedPathsForShares []*regexp.Regexp
}
func getShareManager(c *config, logger *zerolog.Logger) (share.Manager, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver], logger)
}
return nil, errtypes.NotFound("driver not found: " + c.Driver)
}
// TODO(labkode): add ctx to Close.
func (s *service) Close() error {
return nil
}
func (s *service) UnprotectedEndpoints() []string {
return []string{}
}
func (s *service) Register(ss *grpc.Server) {
collaboration.RegisterCollaborationAPIServer(ss, s)
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New creates a new user share provider svc initialized from defaults
func NewDefault(m map[string]any, ss *grpc.Server, logger *zerolog.Logger) (rgrpc.Service, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
sm, err := getShareManager(c, logger)
if err != nil {
return nil, err
}
allowedPathsForShares := make([]*regexp.Regexp, 0, len(c.AllowedPathsForShares))
for _, s := range c.AllowedPathsForShares {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedPathsForShares = append(allowedPathsForShares, regex)
}
gatewaySelector, err := pool.GatewaySelector(sharedconf.GetGatewaySVC(c.GatewayAddr))
if err != nil {
return nil, err
}
return New(gatewaySelector, sm, allowedPathsForShares), nil
}
// New creates a new user share provider svc
func New(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], sm share.Manager, allowedPathsForShares []*regexp.Regexp) rgrpc.Service {
service := &service{
sm: sm,
gatewaySelector: gatewaySelector,
allowedPathsForShares: allowedPathsForShares,
}
return service
}
func (s *service) isPathAllowed(path string) bool {
if len(s.allowedPathsForShares) == 0 {
return true
}
for _, reg := range s.allowedPathsForShares {
if reg.MatchString(path) {
return true
}
}
return false
}
func (s *service) CreateShare(ctx context.Context, req *collaboration.CreateShareRequest) (*collaboration.CreateShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
// check if the grantee is a user or group
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER {
// check if the tenantId of the user matches the tenantId of the target user
if user.GetId().GetTenantId() != req.GetGrant().GetGrantee().GetUserId().GetTenantId() {
log.Warn().Msg("user tenantId does not match the target user tenantId, this is not supported yet")
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "user tenantId does not match the target user tenantId"),
}, nil
}
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
// check if the user has the permission to create shares at all
ok, err := utils.CheckPermission(ctx, permission.WriteShare, gatewayClient)
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write public link"),
}, err
}
if !ok {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create public links"),
}, nil
}
// use logged in user Idp as default, if the Grantee does not have an IDP set.
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER && req.GetGrant().GetGrantee().GetUserId().GetIdp() == "" {
req.GetGrant().GetGrantee().Id = &provider.Grantee_UserId{
UserId: &userpb.UserId{
OpaqueId: req.GetGrant().GetGrantee().GetUserId().GetOpaqueId(),
Idp: user.GetId().GetIdp(),
Type: userpb.UserType_USER_TYPE_PRIMARY},
}
}
// some for group grantees
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_GROUP && req.GetGrant().GetGrantee().GetGroupId().GetIdp() == "" {
// use logged in user Idp as default.
req.GetGrant().GetGrantee().Id = &provider.Grantee_GroupId{
GroupId: &grouppb.GroupId{
OpaqueId: req.GetGrant().GetGrantee().GetGroupId().GetOpaqueId(),
Idp: user.GetId().GetIdp(),
Type: grouppb.GroupType_GROUP_TYPE_REGULAR},
}
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: req.GetResourceInfo().GetId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetResourceInfo().GetId()).Msg("failed to stat resource to share")
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the user needs to have the AddGrant permissions on the Resource to be able to create a share
if !sRes.GetInfo().GetPermissionSet().AddGrant {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to add grants on shared resource"),
}, err
}
isSpaceRoot := utils.IsSpaceRoot(sRes.GetInfo())
var noEvent bool
if isSpaceRoot {
if sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypePersonal || sRes.GetInfo().GetSpace().GetSpaceType() == _spaceTypeVirtual {
return &collaboration.CreateShareResponse{Status: status.NewInvalid(ctx, "space type is not eligible for sharing")}, nil
}
}
// do not allow share to myself or the owner if share is for a user
if req.GetGrant().GetGrantee().GetType() == provider.GranteeType_GRANTEE_TYPE_USER &&
(utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), user.Id) || utils.UserEqual(req.GetGrant().GetGrantee().GetUserId(), sRes.GetInfo().GetOwner())) {
denySelfShare := true
// To allow adding the initial "mananger" share for the creator of a space when need to make an exception here
// if the shared resource is a space root, that does not have any Share existin yet. Note, that we have already
// verified that the user adding the share does really have "AddGrant" permissions on the affected resource, so
// this "hack" should be ok.
if isSpaceRoot {
shares, err := s.sm.ListShares(
ctx,
[]*collaboration.Filter{share.ResourceIDFilter(req.GetResourceInfo().GetId())},
)
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewInternal(ctx, "failed to list existing shares"),
}, nil
}
if len(shares) == 0 {
denySelfShare = false
noEvent = true
}
}
if denySelfShare {
err := errtypes.BadRequest("jsoncs3: owner/creator and grantee are the same")
return nil, err
}
}
// resharing is forbidden for not space roots
if !isSpaceRoot {
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
if HasGrantPermissions(req.GetGrant().GetPermissions().GetPermissions()) {
return &collaboration.CreateShareResponse{
Status: status.NewInvalidArg(ctx, "resharing not supported"),
}, nil
}
}
// check if the share creator has sufficient permissions to do so.
if shareCreationAllowed := conversions.SufficientCS3Permissions(
sRes.GetInfo().GetPermissionSet(),
req.GetGrant().GetPermissions().GetPermissions(),
); !shareCreationAllowed {
return &collaboration.CreateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "insufficient permissions to create that kind of share"),
}, nil
}
// check if the requested permission are plausible for the Resource
if sRes.GetInfo().GetType() == provider.ResourceType_RESOURCE_TYPE_FILE {
if newPermissions := req.GetGrant().GetPermissions().GetPermissions(); newPermissions.GetCreateContainer() || newPermissions.GetMove() || newPermissions.GetDelete() {
return &collaboration.CreateShareResponse{
Status: status.NewInvalid(ctx, "cannot set the requested permissions on that type of resource"),
}, nil
}
}
if !s.isPathAllowed(req.GetResourceInfo().GetPath()) {
return &collaboration.CreateShareResponse{
Status: status.NewFailedPrecondition(ctx, nil, "share creation is not allowed for the specified path"),
}, nil
}
createdShare, err := s.sm.Share(ctx, req.GetResourceInfo(), req.GetGrant())
if err != nil {
return &collaboration.CreateShareResponse{
Status: status.NewStatusFromErrType(ctx, "error creating share", err),
}, nil
}
var opaque *typesv1beta1.Opaque
if isSpaceRoot {
opaque = utils.SpaceGrantOpaque()
}
if noEvent {
opaque = &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"noevent": {},
},
}
}
return &collaboration.CreateShareResponse{
Status: status.NewOK(ctx),
Share: createdShare,
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
}, nil
}
func HasGrantPermissions(p *provider.ResourcePermissions) bool {
return p.GetAddGrant() || p.GetUpdateGrant() || p.GetRemoveGrant() || p.GetDenyGrant()
}
// spaceRootHasRemainingManager returns true if the given resource has at least one share
// (excluding the share with excludeShareOpaqueID) that grants both AddGrant and RemoveGrant.
func (s *service) spaceRootHasRemainingManager(ctx context.Context, resourceID *provider.ResourceId, excludeShareOpaqueID string) (bool, error) {
shares, err := s.sm.ListShares(ctx, []*collaboration.Filter{
{
Type: collaboration.Filter_TYPE_RESOURCE_ID,
Term: &collaboration.Filter_ResourceId{ResourceId: resourceID},
},
})
if err != nil {
return false, err
}
for _, s := range shares {
if s.GetId().GetOpaqueId() == excludeShareOpaqueID {
continue
}
perms := s.GetPermissions().GetPermissions()
if perms.GetAddGrant() && perms.GetRemoveGrant() {
return true, nil
}
}
return false, nil
}
func (s *service) RemoveShare(ctx context.Context, req *collaboration.RemoveShareRequest) (*collaboration.RemoveShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
share, err := s.sm.GetShare(ctx, req.Ref)
if err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error getting share"),
}, nil
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.GetResourceId()}})
if err != nil {
log.Err(err).Interface("resource_id", share.GetResourceId()).Msg("failed to stat shared resource")
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the requesting user needs to be either the Owner/Creator of the share or have the RemoveGrant permissions on the Resource
switch {
case utils.UserEqual(user.GetId(), share.GetCreator()) || utils.UserEqual(user.GetId(), share.GetOwner()):
fallthrough
case sRes.GetInfo().GetPermissionSet().RemoveGrant:
break
default:
return &collaboration.RemoveShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to remove grants on shared resource"),
}, err
}
// For space root shares, ensure at least one share with both AddGrant and RemoveGrant will remain
// so the space always has a manager who can administer grants.
if utils.IsSpaceRoot(sRes.GetInfo()) {
if ok, err := s.spaceRootHasRemainingManager(ctx, share.GetResourceId(), share.GetId().GetOpaqueId()); err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "failed to list shares for space root"),
}, nil
} else if !ok {
return &collaboration.RemoveShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
}, nil
}
}
err = s.sm.Unshare(ctx, req.Ref)
if err != nil {
return &collaboration.RemoveShareResponse{
Status: status.NewInternal(ctx, "error removing share"),
}, nil
}
var opaque *typesv1beta1.Opaque
if utils.IsSpaceRoot(sRes.GetInfo()) {
opaque = utils.SpaceGrantOpaque()
}
opaque = utils.AppendJSONToOpaque(opaque, "resourceid", share.GetResourceId())
opaque = utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName())
if user := share.GetGrantee().GetUserId(); user != nil {
opaque = utils.AppendJSONToOpaque(opaque, "granteeuserid", user)
} else {
opaque = utils.AppendJSONToOpaque(opaque, "granteegroupid", share.GetGrantee().GetGroupId())
}
return &collaboration.RemoveShareResponse{
Opaque: opaque,
Status: status.NewOK(ctx),
}, nil
}
func (s *service) GetShare(ctx context.Context, req *collaboration.GetShareRequest) (*collaboration.GetShareResponse, error) {
share, err := s.sm.GetShare(ctx, req.Ref)
if err != nil {
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &collaboration.GetShareResponse{
Status: st,
}, nil
}
return &collaboration.GetShareResponse{
Status: status.NewOK(ctx),
Share: share,
}, nil
}
func (s *service) ListShares(ctx context.Context, req *collaboration.ListSharesRequest) (*collaboration.ListSharesResponse, error) {
shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager
if err != nil {
return &collaboration.ListSharesResponse{
Status: status.NewInternal(ctx, "error listing shares"),
}, nil
}
res := &collaboration.ListSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShareRequest) (*collaboration.UpdateShareResponse, error) {
log := appctx.GetLogger(ctx)
user := ctxpkg.ContextMustGetUser(ctx)
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
// check if the user has the permission to create shares at all
ok, err := utils.CheckPermission(ctx, permission.WriteShare, gatewayClient)
if err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed check user permission to write share"),
}, err
}
if !ok {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to create user share"),
}, nil
}
// Read share from backend. We need the shared resource's id for STATing it, it might not be in
// the incoming request
currentShare, err := s.sm.GetShare(ctx,
&collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: req.GetShare().GetId(),
},
},
)
if err != nil {
var st *rpc.Status
switch err.(type) {
case errtypes.IsNotFound:
st = status.NewNotFound(ctx, err.Error())
default:
st = status.NewInternal(ctx, err.Error())
}
return &collaboration.UpdateShareResponse{
Status: st,
}, nil
}
sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: currentShare.GetResourceId()}})
if err != nil {
log.Err(err).Interface("resource_id", req.GetShare().GetResourceId()).Msg("failed to stat resource to share")
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed to stat shared resource"),
}, err
}
// the requesting user needs to be either the Owner/Creator of the share or have the UpdateGrant permissions on the Resource
switch {
case utils.UserEqual(user.GetId(), currentShare.GetCreator()) || utils.UserEqual(user.GetId(), currentShare.GetOwner()):
fallthrough
case sRes.GetInfo().GetPermissionSet().UpdateGrant:
break
default:
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "no permission to remove grants on shared resource"),
}, err
}
// resharing is forbidden for not space roots
if !utils.IsSpaceRoot(sRes.GetInfo()) {
// Resharing of Files/Directories is forbidden. So the grants must not allow the "grant" permissions
if HasGrantPermissions(req.GetShare().GetPermissions().GetPermissions()) {
return &collaboration.UpdateShareResponse{
Status: status.NewInvalidArg(ctx, "resharing not supported"),
}, nil
}
}
// If this is a permissions update, check if user's permissions on the resource are sufficient to set the desired permissions
var newPermissions *provider.ResourcePermissions
if slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathPermissions) {
newPermissions = req.GetShare().GetPermissions().GetPermissions()
} else {
newPermissions = req.GetField().GetPermissions().GetPermissions()
}
if newPermissions != nil && !conversions.SufficientCS3Permissions(sRes.GetInfo().GetPermissionSet(), newPermissions) {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "insufficient permissions to create that kind of share"),
}, nil
}
// For space root shares, if the new permissions would strip AddGrant or RemoveGrant from this share,
// ensure at least one other share still has both so the space retains a manager.
if utils.IsSpaceRoot(sRes.GetInfo()) && newPermissions != nil && (!newPermissions.GetAddGrant() || !newPermissions.GetRemoveGrant()) {
if ok, err := s.spaceRootHasRemainingManager(ctx, currentShare.GetResourceId(), currentShare.GetId().GetOpaqueId()); err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "failed to list shares for space root"),
}, nil
} else if !ok {
return &collaboration.UpdateShareResponse{
Status: status.NewPermissionDenied(ctx, nil, "cannot remove the last share with manager permissions on a space root"),
}, nil
}
}
// check if the requested permission are plausible for the Resource
// do we need more here?
if sRes.GetInfo().GetType() == provider.ResourceType_RESOURCE_TYPE_FILE {
if newPermissions.GetCreateContainer() || newPermissions.GetMove() || newPermissions.GetDelete() {
return &collaboration.UpdateShareResponse{
Status: status.NewInvalid(ctx, "cannot set the requested permissions on that type of resource"),
}, nil
}
}
share, err := s.sm.UpdateShare(ctx, req.Ref, req.Field.GetPermissions(), req.Share, req.UpdateMask) // TODO(labkode): check what to update
if err != nil {
return &collaboration.UpdateShareResponse{
Status: status.NewInternal(ctx, "error updating share"),
}, nil
}
var opaque *typesv1beta1.Opaque
if utils.IsSpaceRoot(sRes.GetInfo()) {
opaque = utils.SpaceGrantOpaque()
}
res := &collaboration.UpdateShareResponse{
Status: status.NewOK(ctx),
Share: share,
Opaque: utils.AppendPlainToOpaque(opaque, "resourcename", sRes.GetInfo().GetName()),
}
return res, nil
}
func (s *service) ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest) (*collaboration.ListReceivedSharesResponse, error) {
// For the UI add a filter to not display the denial shares
foundExclude := false
for _, f := range req.Filters {
if f.Type == collaboration.Filter_TYPE_EXCLUDE_DENIALS {
foundExclude = true
break
}
}
if !foundExclude {
req.Filters = append(req.Filters, &collaboration.Filter{Type: collaboration.Filter_TYPE_EXCLUDE_DENIALS})
}
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
shares, err := s.sm.ListReceivedShares(ctx, req.Filters, &uid) // TODO(labkode): check what to update
if err != nil {
return &collaboration.ListReceivedSharesResponse{
Status: status.NewInternal(ctx, "error listing received shares"),
}, nil
}
res := &collaboration.ListReceivedSharesResponse{
Status: status.NewOK(ctx),
Shares: shares,
}
return res, nil
}
func (s *service) GetReceivedShare(ctx context.Context, req *collaboration.GetReceivedShareRequest) (*collaboration.GetReceivedShareResponse, error) {
log := appctx.GetLogger(ctx)
share, err := s.sm.GetReceivedShare(ctx, req.Ref)
if err != nil {
log.Debug().Err(err).Msg("error getting received share")
switch err.(type) {
case errtypes.NotFound:
return &collaboration.GetReceivedShareResponse{
Status: status.NewNotFound(ctx, "error getting received share"),
}, nil
default:
return &collaboration.GetReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
}
res := &collaboration.GetReceivedShareResponse{
Status: status.NewOK(ctx),
Share: share,
}
return res, nil
}
func (s *service) UpdateReceivedShare(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*collaboration.UpdateReceivedShareResponse, error) {
if req.GetShare().GetShare().GetId().GetOpaqueId() == "" {
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInvalid(ctx, "share id empty"),
}, nil
}
isStateTransitionShareAccepted := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathState) && req.GetShare().GetState() == collaboration.ShareState_SHARE_STATE_ACCEPTED
isMountPointSet := slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) && req.GetShare().GetMountPoint().GetPath() != ""
// we calculate a valid mountpoint only if the share should be accepted and the mount point is not set explicitly
if isStateTransitionShareAccepted && !isMountPointSet {
s, err := s.setReceivedShareMountPoint(ctx, req)
switch {
case err != nil:
fallthrough
case s.GetCode() != rpc.Code_CODE_OK:
return &collaboration.UpdateReceivedShareResponse{
Status: s,
}, err
}
}
var uid userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &uid)
updatedShare, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask, &uid)
switch err.(type) {
case nil:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewOK(ctx),
Share: updatedShare,
}, nil
case errtypes.NotFound:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewNotFound(ctx, "error getting received share"),
}, nil
default:
return &collaboration.UpdateReceivedShareResponse{
Status: status.NewInternal(ctx, "error getting received share"),
}, nil
}
}
func (s *service) setReceivedShareMountPoint(ctx context.Context, req *collaboration.UpdateReceivedShareRequest) (*rpc.Status, error) {
gwc, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
receivedShare, err := gwc.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: req.GetShare().GetShare().GetId(),
},
},
})
switch {
case err != nil:
fallthrough
case receivedShare.GetStatus().GetCode() != rpc.Code_CODE_OK:
return receivedShare.GetStatus(), err
}
if receivedShare.GetShare().GetMountPoint().GetPath() != "" {
return status.NewOK(ctx), nil
}
gwc, err = s.gatewaySelector.Next()
if err != nil {
return nil, err
}
resourceStat, err := gwc.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: receivedShare.GetShare().GetShare().GetResourceId(),
},
})
switch {
case err != nil:
fallthrough
case resourceStat.GetStatus().GetCode() != rpc.Code_CODE_OK:
return resourceStat.GetStatus(), err
}
// handle mount point related updates
{
var userID *userpb.UserId
_ = utils.ReadJSONFromOpaque(req.Opaque, "userid", &userID)
receivedShares, err := s.sm.ListReceivedShares(ctx, []*collaboration.Filter{}, userID)
if err != nil {
return nil, err
}
// check if the requested mount point is available and if not, find a suitable one
availableMountpoint, _, err := getMountpointAndUnmountedShares(ctx, receivedShares, s.gatewaySelector, nil,
resourceStat.GetInfo().GetId(),
resourceStat.GetInfo().GetName(),
)
if err != nil {
return status.NewInternal(ctx, err.Error()), nil
}
if !slices.Contains(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint) {
req.GetUpdateMask().Paths = append(req.GetUpdateMask().GetPaths(), _fieldMaskPathMountPoint)
}
req.GetShare().MountPoint = &provider.Reference{
Path: availableMountpoint,
}
}
return status.NewOK(ctx), nil
}
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
func GetMountpointAndUnmountedShares(ctx context.Context, gwc gateway.GatewayAPIClient, id *provider.ResourceId, name string, userId *userpb.UserId) (string, []*collaboration.ReceivedShare, error) {
listReceivedSharesReq := &collaboration.ListReceivedSharesRequest{}
if userId != nil {
listReceivedSharesReq.Opaque = utils.AppendJSONToOpaque(nil, "userid", userId)
}
listReceivedSharesRes, err := gwc.ListReceivedShares(ctx, listReceivedSharesReq)
if err != nil {
return "", nil, errtypes.InternalError("grpc list received shares request failed")
}
if err := errtypes.NewErrtypeFromStatus(listReceivedSharesRes.GetStatus()); err != nil {
return "", nil, err
}
return getMountpointAndUnmountedShares(ctx, listReceivedSharesRes.GetShares(), nil, gwc, id, name)
}
// GetMountpointAndUnmountedShares returns a new or existing mountpoint for the given info and produces a list of unmounted received shares for the same resource
func getMountpointAndUnmountedShares(ctx context.Context, receivedShares []*collaboration.ReceivedShare, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], gwc gateway.GatewayAPIClient, id *provider.ResourceId, name string) (string, []*collaboration.ReceivedShare, error) {
unmountedShares := []*collaboration.ReceivedShare{}
base := filepath.Clean(name)
mount := base
existingMountpoint := ""
mountedShares := make([]string, 0, len(receivedShares))
var pathExists bool
var err error
for _, s := range receivedShares {
resourceIDEqual := utils.ResourceIDEqual(s.GetShare().GetResourceId(), id)
if resourceIDEqual && s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
if gatewaySelector != nil {
gwc, err = gatewaySelector.Next()
if err != nil {
return "", nil, err
}
}
// a share to the resource already exists and is mounted, remembers the mount point
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
if err == nil {
existingMountpoint = s.GetMountPoint().GetPath()
}
}
if resourceIDEqual && s.State != collaboration.ShareState_SHARE_STATE_ACCEPTED {
// a share to the resource already exists but is not mounted, collect the unmounted share
unmountedShares = append(unmountedShares, s)
}
if s.State == collaboration.ShareState_SHARE_STATE_ACCEPTED {
// collect all accepted mount points
mountedShares = append(mountedShares, s.GetMountPoint().GetPath())
if s.GetMountPoint().GetPath() == mount {
// does the shared resource still exist?
if gatewaySelector != nil {
gwc, err = gatewaySelector.Next()
if err != nil {
return "", nil, err
}
}
_, err := utils.GetResourceByID(ctx, s.GetShare().GetResourceId(), gwc)
if err == nil {
pathExists = true
}
// TODO we could delete shares here if the stat returns code NOT FOUND ... but listening for file deletes would be better
}
}
}
if existingMountpoint != "" {
// we want to reuse the same mountpoint for all unmounted shares to the same resource
return existingMountpoint, unmountedShares, nil
}
// If the mount point really already exists, we need to insert a number into the filename
if pathExists {
// now we have a list of shares, we want to iterate over all of them and check for name collisions agents a mount points list
for i := 1; i <= len(mountedShares)+1; i++ {
ext := filepath.Ext(base)
name := strings.TrimSuffix(base, ext)
mount = name + " (" + strconv.Itoa(i) + ")" + ext
if !slices.Contains(mountedShares, mount) {
return mount, unmountedShares, nil
}
}
}
return mount, unmountedShares, nil
}