Bump reva
This commit is contained in:
-2
@@ -32,7 +32,6 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/appauth/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/auth/registry/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/datatx/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/group/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/metrics/driver/loader"
|
||||
@@ -45,7 +44,6 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/rhttp/datatx/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/favorite/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/fs/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/registry/loader"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/token/manager/loader"
|
||||
|
||||
Generated
Vendored
+40
@@ -648,6 +648,46 @@ func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainer
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
c, _, req.Ref, err = s.findAndUnwrap(ctx, req.Ref)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, fmt.Sprintf("gateway could not find space for ref=%+v", req.Ref), err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := c.AddFavorite(ctx, req)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "gateway could not call AddFavorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
c, _, req.Ref, err = s.findAndUnwrap(ctx, req.Ref)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, fmt.Sprintf("gateway could not find space for ref=%+v", req.Ref), err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
res, err := c.RemoveFavorite(ctx, req)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "gateway could not call RemoveFavorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *svc) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) {
|
||||
var c provider.ProviderAPIClient
|
||||
var err error
|
||||
|
||||
Generated
Vendored
+6
@@ -264,3 +264,9 @@ func (c *cachedAPIClient) GetHome(ctx context.Context, in *provider.GetHomeReque
|
||||
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) AddFavorite(ctx context.Context, in *provider.AddFavoriteRequest, opts ...grpc.CallOption) (*provider.AddFavoriteResponse, error) {
|
||||
return c.c.AddFavorite(ctx, in, opts...)
|
||||
}
|
||||
func (c *cachedAPIClient) RemoveFavorite(ctx context.Context, in *provider.RemoveFavoriteRequest, opts ...grpc.CallOption) (*provider.RemoveFavoriteResponse, error) {
|
||||
return c.c.RemoveFavorite(ctx, in, opts...)
|
||||
}
|
||||
|
||||
Generated
Vendored
+8
@@ -929,6 +929,14 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
// resolveToken returns the resource info for the publicly shared resource.
|
||||
func (s *service) resolveToken(ctx context.Context, share interface{}) (*provider.ResourceInfo, interface{}, error) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
|
||||
Generated
Vendored
+8
@@ -1035,6 +1035,14 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented")
|
||||
}
|
||||
|
||||
func (s *service) resolveAcceptedShare(ctx context.Context, ref *provider.Reference) (*collaboration.ReceivedShare, *rpc.Status, error) {
|
||||
// treat absolute id based references as relative ones
|
||||
if ref.Path == "" {
|
||||
|
||||
Generated
Vendored
+30
@@ -706,6 +706,36 @@ func (s *Service) TouchFile(ctx context.Context, req *provider.TouchFileRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AddFavorite(ctx context.Context, req *provider.AddFavoriteRequest) (*provider.AddFavoriteResponse, error) {
|
||||
appctx.GetLogger(ctx).Debug().Msg("AddFavorite")
|
||||
|
||||
err := s.Storage.AddFavorite(ctx, req.Ref, req.UserId)
|
||||
if err != nil {
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "add favorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &provider.AddFavoriteResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) RemoveFavorite(ctx context.Context, req *provider.RemoveFavoriteRequest) (*provider.RemoveFavoriteResponse, error) {
|
||||
appctx.GetLogger(ctx).Debug().Msg("RemoveFavorite")
|
||||
|
||||
err := s.Storage.RemoveFavorite(ctx, req.Ref, req.UserId)
|
||||
if err != nil {
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewStatusFromErrType(ctx, "remove favorite", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &provider.RemoveFavoriteResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) {
|
||||
if req.Ref.GetPath() == "/" {
|
||||
return &provider.DeleteResponse{
|
||||
|
||||
Generated
Vendored
+9
-15
@@ -21,17 +21,15 @@ type Config struct {
|
||||
Timeout int64 `mapstructure:"timeout"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
// If true, HTTP COPY will expect the HTTP-TPC (third-party copy) headers
|
||||
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
|
||||
PublicURL string `mapstructure:"public_url"`
|
||||
FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"`
|
||||
FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"`
|
||||
Version string `mapstructure:"version"`
|
||||
VersionString string `mapstructure:"version_string"`
|
||||
Edition string `mapstructure:"edition"`
|
||||
Product string `mapstructure:"product"`
|
||||
ProductName string `mapstructure:"product_name"`
|
||||
ProductVersion string `mapstructure:"product_version"`
|
||||
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
|
||||
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
|
||||
PublicURL string `mapstructure:"public_url"`
|
||||
Version string `mapstructure:"version"`
|
||||
VersionString string `mapstructure:"version_string"`
|
||||
Edition string `mapstructure:"edition"`
|
||||
Product string `mapstructure:"product"`
|
||||
ProductName string `mapstructure:"product_name"`
|
||||
ProductVersion string `mapstructure:"product_version"`
|
||||
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
|
||||
|
||||
NameValidation NameValidation `mapstructure:"validation"`
|
||||
|
||||
@@ -52,10 +50,6 @@ func (c *Config) Init() {
|
||||
// note: default c.Prefix is an empty string
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
|
||||
if c.FavoriteStorageDriver == "" {
|
||||
c.FavoriteStorageDriver = "memory"
|
||||
}
|
||||
|
||||
if c.Version == "" {
|
||||
c.Version = "10.0.11.5"
|
||||
}
|
||||
|
||||
Generated
Vendored
+7
-21
@@ -43,8 +43,6 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -61,12 +59,11 @@ func init() {
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
c *config.Config
|
||||
webDavHandler *WebDavHandler
|
||||
davHandler *DavHandler
|
||||
favoritesManager favorite.Manager
|
||||
client *http.Client
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
c *config.Config
|
||||
webDavHandler *WebDavHandler
|
||||
davHandler *DavHandler
|
||||
client *http.Client
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
// LockSystem is the lock management system.
|
||||
LockSystem LockSystem
|
||||
userIdentifierCache *ttlcache.Cache
|
||||
@@ -78,12 +75,6 @@ func (s *svc) Config() *config.Config {
|
||||
return s.c
|
||||
}
|
||||
|
||||
func getFavoritesManager(c *config.Config) (favorite.Manager, error) {
|
||||
if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
|
||||
return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
|
||||
}
|
||||
return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
|
||||
}
|
||||
func getLockSystem(c *config.Config) (LockSystem, error) {
|
||||
// TODO in memory implementation
|
||||
selector, err := pool.GatewaySelector(c.GatewaySvc)
|
||||
@@ -102,20 +93,16 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error)
|
||||
|
||||
conf.Init()
|
||||
|
||||
fm, err := getFavoritesManager(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ls, err := getLockSystem(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewWith(conf, fm, ls, log, nil)
|
||||
return NewWith(conf, ls, log, nil)
|
||||
}
|
||||
|
||||
// NewWith returns a new ocdav service
|
||||
func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
|
||||
func NewWith(conf *config.Config, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
|
||||
// be safe - init the conf again
|
||||
conf.Init()
|
||||
|
||||
@@ -137,7 +124,6 @@ func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog
|
||||
rhttp.Insecure(conf.Insecure),
|
||||
),
|
||||
gatewaySelector: selector,
|
||||
favoritesManager: fm,
|
||||
LockSystem: ls,
|
||||
userIdentifierCache: ttlcache.NewCache(),
|
||||
nameValidators: ValidatorsFromConfig(conf),
|
||||
|
||||
Generated
Vendored
-52
@@ -36,11 +36,8 @@ import (
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
|
||||
"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/permission"
|
||||
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
@@ -214,30 +211,6 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
errors.HandleWebdavError(&log, w, b, err)
|
||||
return nil, nil, false
|
||||
}
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to unset favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
removedProps = append(removedProps, propNameXML)
|
||||
} else {
|
||||
sreq.ArbitraryMetadata.Metadata[key] = value
|
||||
@@ -282,31 +255,6 @@ func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *htt
|
||||
|
||||
acceptedProps = append(acceptedProps, propNameXML)
|
||||
delete(sreq.ArbitraryMetadata.Metadata, key)
|
||||
|
||||
if key == "http://owncloud.org/ns/favorite" {
|
||||
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
|
||||
if err != nil || statRes.Info == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to set favorite")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return nil, nil, false
|
||||
}
|
||||
err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return nil, nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FIXME: in case of error, need to set all properties back to the original state,
|
||||
|
||||
Generated
Vendored
-80
@@ -23,14 +23,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
|
||||
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/permission"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,13 +48,6 @@ func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
|
||||
return
|
||||
}
|
||||
|
||||
if rep.FilterFiles != nil {
|
||||
s.doFilterFiles(w, r, rep.FilterFiles, ns)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(jfd): implement report
|
||||
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
@@ -68,73 +55,6 @@ func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSe
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) {
|
||||
ctx := r.Context()
|
||||
log := appctx.GetLogger(ctx)
|
||||
|
||||
if ff.Rules.Favorite {
|
||||
// List the users favorite resources.
|
||||
client, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error selecting next gateway client")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
currentUser := ctxpkg.ContextMustGetUser(ctx)
|
||||
ok, err := utils.CheckPermission(ctx, permission.ListFavorites, client)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error checking permission")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
log.Info().Interface("user", currentUser).Msg("user not allowed to list favorites")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
favorites, err := s.favoritesManager.ListFavorites(ctx, currentUser.Id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting favorites")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
infos := make([]*providerv1beta1.ResourceInfo, 0, len(favorites))
|
||||
for i := range favorites {
|
||||
statRes, err := client.Stat(ctx, &providerv1beta1.StatRequest{Ref: &providerv1beta1.Reference{ResourceId: favorites[i]}})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
log.Error().Interface("stat_response", statRes).Msg("error getting resource info")
|
||||
continue
|
||||
}
|
||||
infos = append(infos, statRes.Info)
|
||||
}
|
||||
|
||||
prefer := net.ParsePrefer(r.Header.Get("prefer"))
|
||||
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
|
||||
|
||||
responsesXML, err := propfind.MultistatusResponse(ctx, &propfind.XML{Prop: ff.Prop}, infos, s.c.PublicURL, namespace, nil, returnMinimal, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error formatting propfind")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
|
||||
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
|
||||
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
|
||||
if returnMinimal {
|
||||
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
|
||||
}
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
if _, err := w.Write(responsesXML); err != nil {
|
||||
log.Err(err).Msg("error writing response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type report struct {
|
||||
SearchFiles *reportSearchFiles
|
||||
// FilterFiles TODO add this for tag based search
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
// 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 cbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New returns an instance of the cbox sql favorites manager.
|
||||
func New(m map[string]interface{}) (favorite.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListFavorites(ctx context.Context, userID *user.UserId) ([]*provider.ResourceId, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
infos := []*provider.ResourceId{}
|
||||
query := `SELECT fileid_prefix, fileid FROM cbox_metadata WHERE uid=? AND tag_key="fav"`
|
||||
rows, err := m.db.Query(query, user.Id.OpaqueId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var info provider.ResourceId
|
||||
if err := rows.Scan(&info.SpaceId, &info.OpaqueId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = append(infos, &info)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
spaceID := resourceInfo.Id.SpaceId
|
||||
|
||||
// The primary key is just the ID in the table, it should ideally be (uid, fileid_prefix, fileid, tag_key)
|
||||
// For the time being, just check if the favorite already exists. If it does, return early
|
||||
var id int
|
||||
query := `SELECT id FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`
|
||||
if err := m.db.QueryRow(query, user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId).Scan(&id); err == nil {
|
||||
// Favorite is already set, return
|
||||
return nil
|
||||
}
|
||||
|
||||
query = `INSERT INTO cbox_metadata SET item_type=?, uid=?, fileid_prefix=?, fileid=?, tag_key="fav"`
|
||||
vals := []interface{}{utils.ResourceTypeToItemInt(resourceInfo.Type), user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId}
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = stmt.Exec(vals...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UnsetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
spaceID := resourceInfo.Id.SpaceId
|
||||
|
||||
stmt, err := m.db.Prepare(`DELETE FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := stmt.Exec(user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-221
@@ -1,221 +0,0 @@
|
||||
// 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 rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
groupPrefix = "group:"
|
||||
idPrefix = "id:"
|
||||
namePrefix = "name:"
|
||||
gidPrefix = "gid:"
|
||||
groupMembersPrefix = "members:"
|
||||
groupInternalIDPrefix = "internal:"
|
||||
)
|
||||
|
||||
func initRedisPool(address, username, password string) *redis.Pool {
|
||||
return &redis.Pool{
|
||||
|
||||
MaxIdle: 50,
|
||||
MaxActive: 1000,
|
||||
IdleTimeout: 240 * time.Second,
|
||||
|
||||
Dial: func() (redis.Conn, error) {
|
||||
var c redis.Conn
|
||||
var err error
|
||||
switch {
|
||||
case username != "":
|
||||
c, err = redis.Dial("tcp", address,
|
||||
redis.DialUsername(username),
|
||||
redis.DialPassword(password),
|
||||
)
|
||||
case password != "":
|
||||
c, err = redis.Dial("tcp", address,
|
||||
redis.DialPassword(password),
|
||||
)
|
||||
default:
|
||||
c, err = redis.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) setVal(key, val string, expiration int) error {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
args := []interface{}{key, val}
|
||||
if expiration != -1 {
|
||||
args = append(args, "EX", expiration)
|
||||
}
|
||||
if _, err := conn.Do("SET", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) getVal(key string) (string, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
val, err := redis.String(conn.Do("GET", key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedInternalID(gid *grouppb.GroupId) (string, error) {
|
||||
return m.getVal(groupPrefix + groupInternalIDPrefix + gid.OpaqueId)
|
||||
}
|
||||
|
||||
func (m *manager) cacheInternalID(gid *grouppb.GroupId, internalID string) error {
|
||||
return m.setVal(groupPrefix+groupInternalIDPrefix+gid.OpaqueId, internalID, -1)
|
||||
}
|
||||
|
||||
func (m *manager) findCachedGroups(query string) ([]*grouppb.Group, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
query = fmt.Sprintf("%s*%s*", groupPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
|
||||
keys, err := redis.Strings(conn.Do("KEYS", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var args []interface{}
|
||||
for _, k := range keys {
|
||||
args = append(args, k)
|
||||
}
|
||||
|
||||
// Fetch the groups for all these keys
|
||||
groupStrings, err := redis.Strings(conn.Do("MGET", args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupMap := make(map[string]*grouppb.Group)
|
||||
for _, group := range groupStrings {
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err == nil {
|
||||
groupMap[g.Id.OpaqueId] = &g
|
||||
}
|
||||
}
|
||||
|
||||
var groups []*grouppb.Group
|
||||
for _, g := range groupMap {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupDetails(gid *grouppb.GroupId) (*grouppb.Group, error) {
|
||||
group, err := m.getVal(groupPrefix + idPrefix + gid.OpaqueId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheGroupDetails(g *grouppb.Group) error {
|
||||
encodedGroup, err := json.Marshal(&g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(groupPrefix+idPrefix+strings.ToLower(g.Id.OpaqueId), string(encodedGroup), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.GidNumber != 0 {
|
||||
if err = m.setVal(groupPrefix+gidPrefix+strconv.FormatInt(g.GidNumber, 10), g.Id.OpaqueId, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.DisplayName != "" {
|
||||
if err = m.setVal(groupPrefix+namePrefix+g.Id.OpaqueId+"_"+strings.ToLower(g.DisplayName), g.Id.OpaqueId, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupByParam(field, claim string) (*grouppb.Group, error) {
|
||||
group, err := m.getVal(groupPrefix + field + ":" + strings.ToLower(claim))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := grouppb.Group{}
|
||||
if err = json.Unmarshal([]byte(group), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedGroupMembers(gid *grouppb.GroupId) ([]*userpb.UserId, error) {
|
||||
members, err := m.getVal(groupPrefix + groupMembersPrefix + strings.ToLower(gid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u := []*userpb.UserId{}
|
||||
if err = json.Unmarshal([]byte(members), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheGroupMembers(gid *grouppb.GroupId, members []*userpb.UserId) error {
|
||||
u, err := json.Marshal(&members)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.setVal(groupPrefix+groupMembersPrefix+strings.ToLower(gid.OpaqueId), string(u), m.conf.GroupMembersCacheExpiration*60)
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
// 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("rest", New)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
redisPool *redis.Pool
|
||||
apiTokenManager *utils.APITokenManager
|
||||
}
|
||||
|
||||
type config struct {
|
||||
// The address at which the redis server is running
|
||||
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
|
||||
// The username for connecting to the redis server
|
||||
RedisUsername string `mapstructure:"redis_username" docs:""`
|
||||
// The password for connecting to the redis server
|
||||
RedisPassword string `mapstructure:"redis_password" docs:""`
|
||||
// The time in minutes for which the members of a group would be cached
|
||||
GroupMembersCacheExpiration int `mapstructure:"group_members_cache_expiration" docs:"5"`
|
||||
// The OIDC Provider
|
||||
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
|
||||
// Base API Endpoint
|
||||
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
|
||||
// Client ID needed to authenticate
|
||||
ClientID string `mapstructure:"client_id" docs:"-"`
|
||||
// Client Secret
|
||||
ClientSecret string `mapstructure:"client_secret" docs:"-"`
|
||||
|
||||
// Endpoint to generate token to access the API
|
||||
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
|
||||
// The target application for which token needs to be generated
|
||||
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
|
||||
// The time in seconds between bulk fetch of groups
|
||||
GroupFetchInterval int `mapstructure:"group_fetch_interval" docs:"3600"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.GroupMembersCacheExpiration == 0 {
|
||||
c.GroupMembersCacheExpiration = 5
|
||||
}
|
||||
if c.RedisAddress == "" {
|
||||
c.RedisAddress = ":6379"
|
||||
}
|
||||
if c.APIBaseURL == "" {
|
||||
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
|
||||
}
|
||||
if c.TargetAPI == "" {
|
||||
c.TargetAPI = "authorization-service-api"
|
||||
}
|
||||
if c.OIDCTokenEndpoint == "" {
|
||||
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
|
||||
}
|
||||
if c.IDProvider == "" {
|
||||
c.IDProvider = "http://cernbox.cern.ch"
|
||||
}
|
||||
if c.GroupFetchInterval == 0 {
|
||||
c.GroupFetchInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that makes calls to the GRAPPA API.
|
||||
func New(m map[string]interface{}) (group.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
|
||||
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
|
||||
|
||||
mgr := &manager{
|
||||
conf: c,
|
||||
redisPool: redisPool,
|
||||
apiTokenManager: apiTokenManager,
|
||||
}
|
||||
go mgr.fetchAllGroups()
|
||||
return mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllGroups() {
|
||||
_ = m.fetchAllGroupAccounts()
|
||||
ticker := time.NewTicker(time.Duration(m.conf.GroupFetchInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.fetchAllGroupAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllGroupAccounts() error {
|
||||
ctx := context.Background()
|
||||
url := fmt.Sprintf("%s/api/v1.0/Group?field=groupIdentifier&field=displayName&field=gid", m.conf.APIBaseURL)
|
||||
|
||||
for url != "" {
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData, ok := result["data"].([]interface{})
|
||||
if !ok {
|
||||
return errors.New("rest: error in type assertion")
|
||||
}
|
||||
for _, usr := range responseData {
|
||||
groupData, ok := usr.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = m.parseAndCacheGroup(ctx, groupData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
url = ""
|
||||
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
|
||||
if links, ok := pagination["links"].(map[string]interface{}); ok {
|
||||
if next, ok := links["next"].(string); ok {
|
||||
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) parseAndCacheGroup(ctx context.Context, groupData map[string]interface{}) (*grouppb.Group, error) {
|
||||
id, ok := groupData["groupIdentifier"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("rest: missing upn in user data")
|
||||
}
|
||||
|
||||
name, _ := groupData["displayName"].(string)
|
||||
groupID := &grouppb.GroupId{
|
||||
OpaqueId: id,
|
||||
Idp: m.conf.IDProvider,
|
||||
}
|
||||
gid, ok := groupData["gid"].(int64)
|
||||
if !ok {
|
||||
gid = 0
|
||||
}
|
||||
g := &grouppb.Group{
|
||||
Id: groupID,
|
||||
GroupName: id,
|
||||
Mail: id + "@cern.ch",
|
||||
DisplayName: name,
|
||||
GidNumber: gid,
|
||||
}
|
||||
|
||||
if err := m.cacheGroupDetails(g); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching group details")
|
||||
}
|
||||
|
||||
if internalID, ok := groupData["id"].(string); ok {
|
||||
if err := m.cacheInternalID(groupID, internalID); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching group details")
|
||||
}
|
||||
}
|
||||
|
||||
return g, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
|
||||
g, err := m.fetchCachedGroupDetails(gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingMembers {
|
||||
groupMembers, err := m.GetMembers(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Members = groupMembers
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
|
||||
if claim == "group_name" {
|
||||
return m.GetGroup(ctx, &grouppb.GroupId{OpaqueId: value}, skipFetchingMembers)
|
||||
}
|
||||
|
||||
g, err := m.fetchCachedGroupByParam(claim, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingMembers {
|
||||
groupMembers, err := m.GetMembers(ctx, g.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Members = groupMembers
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
|
||||
|
||||
// Look at namespaces filters. If the query starts with:
|
||||
// "a" or none => get egroups
|
||||
// other filters => get empty list
|
||||
|
||||
parts := strings.SplitN(query, ":", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "a" {
|
||||
query = parts[1]
|
||||
} else {
|
||||
return []*grouppb.Group{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return m.findCachedGroups(query)
|
||||
}
|
||||
|
||||
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
|
||||
|
||||
users, err := m.fetchCachedGroupMembers(gid)
|
||||
if err == nil {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
internalID, err := m.fetchCachedInternalID(gid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
url := fmt.Sprintf("%s/api/v1.0/Group/%s/memberidentities/precomputed", m.conf.APIBaseURL, internalID)
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userData := result["data"].([]interface{})
|
||||
users = []*userpb.UserId{}
|
||||
|
||||
for _, u := range userData {
|
||||
userInfo, ok := u.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("rest: error in type assertion")
|
||||
}
|
||||
if id, ok := userInfo["upn"].(string); ok {
|
||||
users = append(users, &userpb.UserId{OpaqueId: id, Idp: m.conf.IDProvider})
|
||||
}
|
||||
}
|
||||
|
||||
if err = m.cacheGroupMembers(gid, users); err != nil {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Error().Err(err).Msg("rest: error caching group members")
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
|
||||
groupMemers, err := m.GetMembers(ctx, gid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, u := range groupMemers {
|
||||
if uid.OpaqueId == u.OpaqueId {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
// 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 cbox specific drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/favorite/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/group/rest"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/preferences/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/publicshare/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/share/sql"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoshomewrapper"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoswrapper"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/user/rest"
|
||||
)
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
// 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 sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New returns an instance of the cbox sql preferences manager.
|
||||
func New(m map[string]interface{}) (preferences.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetKey(ctx context.Context, key, namespace, value string) error {
|
||||
user, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
return errtypes.UserRequired("preferences: error getting user from ctx")
|
||||
}
|
||||
query := `INSERT INTO oc_preferences(userid, appid, configkey, configvalue) values(?, ?, ?, ?) ON DUPLICATE KEY UPDATE configvalue = ?`
|
||||
params := []interface{}{user.Id.OpaqueId, namespace, key, value, value}
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetKey(ctx context.Context, key, namespace string) (string, error) {
|
||||
user, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
return "", errtypes.UserRequired("preferences: error getting user from ctx")
|
||||
}
|
||||
query := `SELECT configvalue FROM oc_preferences WHERE userid=? AND appid=? AND configkey=?`
|
||||
var val string
|
||||
if err := m.db.QueryRow(query, user.Id.OpaqueId, namespace, key).Scan(&val); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", errtypes.NotFound(namespace + ":" + key)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
-533
@@ -1,533 +0,0 @@
|
||||
// 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 sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"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/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const publicShareType = 3
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
|
||||
JanitorRunInterval int `mapstructure:"janitor_run_interval"`
|
||||
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.SharePasswordHashCost == 0 {
|
||||
c.SharePasswordHashCost = 11
|
||||
}
|
||||
if c.JanitorRunInterval == 0 {
|
||||
c.JanitorRunInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) startJanitorRun() {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Duration(m.c.JanitorRunInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.cleanupExpiredShares()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new public share manager.
|
||||
func New(m map[string]interface{}) (publicshare.Manager, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.init()
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr := manager{
|
||||
c: c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}
|
||||
go mgr.startJanitorRun()
|
||||
|
||||
return &mgr, nil
|
||||
}
|
||||
|
||||
func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
|
||||
|
||||
tkn := utils.RandString(15)
|
||||
now := time.Now().Unix()
|
||||
|
||||
displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"]
|
||||
if !ok {
|
||||
displayName = tkn
|
||||
}
|
||||
createdAt := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
creator := conversions.FormatUserID(u.Id)
|
||||
owner := conversions.FormatUserID(rInfo.Owner)
|
||||
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
|
||||
itemType := conversions.ResourceTypeToItem(rInfo.Type)
|
||||
prefix := rInfo.Id.SpaceId
|
||||
itemSource := rInfo.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?"
|
||||
params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName}
|
||||
|
||||
var passwordProtected bool
|
||||
password := g.Password
|
||||
if password != "" {
|
||||
password, err = hashPassword(password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
passwordProtected = true
|
||||
|
||||
query += ",share_with=?"
|
||||
params = append(params, password)
|
||||
}
|
||||
|
||||
if g.Expiration != nil && g.Expiration.Seconds != 0 {
|
||||
t := time.Unix(int64(g.Expiration.Seconds), 0)
|
||||
query += ",expiration=?"
|
||||
params = append(params, t)
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
Owner: rInfo.GetOwner(),
|
||||
Creator: u.Id,
|
||||
ResourceId: rInfo.Id,
|
||||
Token: tkn,
|
||||
Permissions: g.Permissions,
|
||||
Ctime: createdAt,
|
||||
Mtime: createdAt,
|
||||
PasswordProtected: passwordProtected,
|
||||
Expiration: g.Expiration,
|
||||
DisplayName: displayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
|
||||
query := "update oc_share set "
|
||||
paramsMap := map[string]interface{}{}
|
||||
params := []interface{}{}
|
||||
|
||||
now := time.Now().Unix()
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
|
||||
switch req.GetUpdate().GetType() {
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
|
||||
paramsMap["share_name"] = req.Update.GetDisplayName()
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
|
||||
paramsMap["permissions"] = conversions.SharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
|
||||
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
|
||||
if req.Update.GetGrant().Password == "" {
|
||||
paramsMap["share_with"] = ""
|
||||
} else {
|
||||
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
paramsMap["share_with"] = h
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
|
||||
}
|
||||
|
||||
for k, v := range paramsMap {
|
||||
query += k + "=?"
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Ref.GetId() != nil:
|
||||
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
|
||||
case req.Ref.GetToken() != "":
|
||||
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(req.Ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetPublicShare(ctx, u, req.Ref, false)
|
||||
}
|
||||
|
||||
func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) {
|
||||
s := conversions.DBShare{Token: token}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?"
|
||||
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", errtypes.NotFound(token)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return share, s.ShareWith, nil
|
||||
}
|
||||
|
||||
func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) {
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
s := conversions.DBShare{ID: id.OpaqueId}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)"
|
||||
if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, "", errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, "", err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return share, s.ShareWith, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
|
||||
var s *link.PublicShare
|
||||
var pw string
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, pw, err = m.getByID(ctx, ref.GetId(), u)
|
||||
case ref.GetToken() != "":
|
||||
s, pw, err = m.getByToken(ctx, ref.GetToken(), u)
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expired(s) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if s.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(s, pw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=?)"
|
||||
var resourceFilters, ownerFilters, creatorFilters string
|
||||
var resourceParams, ownerParams, creatorParams []interface{}
|
||||
params := []interface{}{uid, uid, publicShareType}
|
||||
|
||||
for _, f := range filters {
|
||||
switch f.Type {
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
|
||||
if len(resourceFilters) != 0 {
|
||||
resourceFilters += " OR "
|
||||
}
|
||||
resourceFilters += "(fileid_prefix=? AND item_source=?)"
|
||||
resourceParams = append(resourceParams, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
|
||||
if len(ownerFilters) != 0 {
|
||||
ownerFilters += " OR "
|
||||
}
|
||||
ownerFilters += "(uid_owner=?)"
|
||||
ownerParams = append(ownerParams, conversions.FormatUserID(f.GetOwner()))
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
|
||||
if len(creatorFilters) != 0 {
|
||||
creatorFilters += " OR "
|
||||
}
|
||||
creatorFilters += "(uid_initiator=?)"
|
||||
creatorParams = append(creatorParams, conversions.FormatUserID(f.GetCreator()))
|
||||
}
|
||||
}
|
||||
if resourceFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
|
||||
params = append(params, resourceParams...)
|
||||
}
|
||||
if ownerFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
|
||||
params = append(params, ownerParams...)
|
||||
}
|
||||
if creatorFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
|
||||
params = append(params, creatorParams...)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s conversions.DBShare
|
||||
shares := []*link.PublicShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
|
||||
continue
|
||||
}
|
||||
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expired(cs3Share) {
|
||||
_ = m.cleanupExpiredShares()
|
||||
} else {
|
||||
if cs3Share.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shares = append(shares, cs3Share)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
|
||||
uid := conversions.FormatUserID(u.Id)
|
||||
query := "delete from oc_share where "
|
||||
params := []interface{}{}
|
||||
|
||||
switch {
|
||||
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
|
||||
query += "id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetToken() != "":
|
||||
query += "token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
|
||||
s := conversions.DBShare{Token: token}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?"
|
||||
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(token)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.ShareWith != "" {
|
||||
if !authenticate(cs3Share, s.ShareWith, auth) {
|
||||
// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
|
||||
return nil, errtypes.InvalidCredentials(token)
|
||||
}
|
||||
|
||||
if sign {
|
||||
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expired(cs3Share) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound(token)
|
||||
}
|
||||
|
||||
return cs3Share, nil
|
||||
}
|
||||
|
||||
func (m *manager) cleanupExpiredShares() error {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := "update oc_share set orphan = 1 where expiration IS NOT NULL AND expiration < ?"
|
||||
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expired(s *link.PublicShare) bool {
|
||||
if s.Expiration != nil {
|
||||
if t := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())); t.Before(time.Now()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hashPassword(password string, cost int) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
return "1|" + string(bytes), err
|
||||
}
|
||||
|
||||
func checkPasswordHash(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
|
||||
switch {
|
||||
case auth.GetPassword() != "":
|
||||
return checkPasswordHash(auth.GetPassword(), pw)
|
||||
case auth.GetSignature() != nil:
|
||||
sig := auth.GetSignature()
|
||||
now := time.Now()
|
||||
expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos()))
|
||||
if now.After(expiration) {
|
||||
return false
|
||||
}
|
||||
s, err := publicshare.CreateSignature(share.Token, pw, expiration)
|
||||
if err != nil {
|
||||
// TODO(labkode): pass context to call to log err.
|
||||
// No we are blind
|
||||
return false
|
||||
}
|
||||
return sig.GetSignature() == s
|
||||
}
|
||||
return false
|
||||
}
|
||||
-592
@@ -1,592 +0,0 @@
|
||||
// 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 sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
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/share"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
shareTypeUser = 0
|
||||
shareTypeGroup = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
// New returns a new share manager.
|
||||
func New(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}, 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 (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errors.New("sql: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
_, err := m.getByKey(ctx, key)
|
||||
|
||||
// share already exists
|
||||
if err == nil {
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
shareType, shareWith := conversions.FormatGrantee(g.Grantee)
|
||||
itemType := conversions.ResourceTypeToItem(md.Type)
|
||||
targetPath := path.Join("/", path.Base(md.Path))
|
||||
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
|
||||
prefix := md.Id.SpaceId
|
||||
itemSource := md.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
stmtString := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,share_with=?,file_target=?"
|
||||
stmtValues := []interface{}{shareType, conversions.FormatUserID(md.Owner), conversions.FormatUserID(user.Id), itemType, prefix, itemSource, fileSource, permissions, now, shareWith, targetPath}
|
||||
|
||||
stmt, err := m.db.Prepare(stmtString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(stmtValues...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
s := conversions.DBShare{ID: id.OpaqueId}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
|
||||
s := conversions.DBShare{}
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRow(query, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
var s *collaboration.Share
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "delete from oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
query = "delete from oc_share where uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
permissions := conversions.SharePermToInt(p.Permissions)
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
owner := conversions.FormatUserID(key.Owner)
|
||||
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetShare(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
|
||||
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
|
||||
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type,
|
||||
id, stime, permissions, share_type
|
||||
FROM oc_share
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)`
|
||||
params := []interface{}{uid, uid, shareTypeUser, shareTypeGroup}
|
||||
|
||||
if len(filters) > 0 {
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s conversions.DBShare
|
||||
shares := []*collaboration.Share{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, _ *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
params := []interface{}{uid, uid, uid, uid}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
query := `SELECT coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
|
||||
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
|
||||
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner != ? AND uid_initiator != ?)`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s conversions.DBShare
|
||||
shares := []*collaboration.ReceivedShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
params := []interface{}{uid, id.OpaqueId, uid} // nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
s := conversions.DBShare{ID: id.OpaqueId}
|
||||
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
|
||||
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
|
||||
stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND ts.id=?`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := conversions.FormatUserID(user.Id)
|
||||
|
||||
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
|
||||
params := []interface{}{uid, conversions.FormatUserID(key.Owner), key.GetResourceId().SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith} // nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
s := conversions.DBShare{}
|
||||
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
|
||||
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
|
||||
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
|
||||
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
|
||||
WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=?`
|
||||
if len(user.Groups) > 0 {
|
||||
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
query += " AND (share_with=? AND share_type = 0)"
|
||||
}
|
||||
|
||||
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return share, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
var s *collaboration.ReceivedShare
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getReceivedByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getReceivedByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: share.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = share.State
|
||||
case "mount_point":
|
||||
rs.MountPoint = share.MountPoint
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
state := 0
|
||||
switch rs.GetState() {
|
||||
case collaboration.ShareState_SHARE_STATE_REJECTED:
|
||||
state = -1
|
||||
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
|
||||
state = 1
|
||||
}
|
||||
|
||||
params := []interface{}{rs.Share.Id.OpaqueId, conversions.FormatUserID(user.Id), state, state}
|
||||
query := "insert into oc_share_status(id, recipient, state) values(?, ?, ?) ON DUPLICATE KEY UPDATE state = ?"
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func granteeTypeToShareType(granteeType provider.GranteeType) int {
|
||||
switch granteeType {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
return shareTypeUser
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
return shareTypeGroup
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// translateFilters translates the filters to sql queries
|
||||
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
|
||||
var (
|
||||
filterQuery string
|
||||
params []interface{}
|
||||
)
|
||||
|
||||
groupedFilters := share.GroupFiltersByType(filters)
|
||||
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
|
||||
// That is why the filters got grouped by type.
|
||||
// For every given filter type, iterate over the filters and if there are more than one combine them.
|
||||
// Combine the different filter types using `AND`
|
||||
var filterCounter = 0
|
||||
for filterType, filters := range groupedFilters {
|
||||
switch filterType {
|
||||
case collaboration.Filter_TYPE_RESOURCE_ID:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "(fileid_prefix =? AND item_source=?)"
|
||||
params = append(params, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_GRANTEE_TYPE:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "share_type=?"
|
||||
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
|
||||
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
|
||||
filterQuery += "(permissions > 0)"
|
||||
default:
|
||||
return "", nil, fmt.Errorf("filter type is not supported")
|
||||
}
|
||||
if filterCounter != len(groupedFilters)-1 {
|
||||
filterQuery += " AND "
|
||||
}
|
||||
filterCounter++
|
||||
}
|
||||
return filterQuery, params, nil
|
||||
}
|
||||
Generated
Vendored
-129
@@ -1,129 +0,0 @@
|
||||
// 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 eoshomewrapper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("eoshomewrapper", New)
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
storage.FS
|
||||
mountIDTemplate *template.Template
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
|
||||
c := &eosfs.Config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// default to version invariance if not configured
|
||||
if _, ok := m["version_invariant"]; !ok {
|
||||
c.VersionInvariant = true
|
||||
}
|
||||
|
||||
t, ok := m["mount_id_template"].(string)
|
||||
if !ok || t == "" {
|
||||
t = "eoshome-{{substr 0 1 .Username}}"
|
||||
}
|
||||
|
||||
return c, t, nil
|
||||
}
|
||||
|
||||
// New returns an implementation of the storage.FS interface that forms a wrapper
|
||||
// around separate connections to EOS.
|
||||
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
|
||||
c, t, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.EnableHome = true
|
||||
|
||||
eos, err := eosfs.NewEOSFS(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper{FS: eos, mountIDTemplate: mountIDTemplate}, nil
|
||||
}
|
||||
|
||||
// We need to override the two methods, GetMD and ListFolder to fill the
|
||||
// StorageId in the ResourceInfo objects.
|
||||
|
||||
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to extract the mount ID based on the mapping template.
|
||||
//
|
||||
// Take the first letter of the username of the logged-in user, as the home
|
||||
// storage provider restricts requests only to the home namespace.
|
||||
res.Id.StorageId = w.getMountID(ctx, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range res {
|
||||
r.Id.StorageId = w.getMountID(ctx, r)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
|
||||
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
|
||||
}
|
||||
|
||||
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
b := bytes.Buffer{}
|
||||
if err := w.mountIDTemplate.Execute(&b, u); err != nil {
|
||||
return ""
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
-296
@@ -1,296 +0,0 @@
|
||||
// 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 eoswrapper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("eoswrapper", New)
|
||||
}
|
||||
|
||||
const (
|
||||
eosProjectsNamespace = "/eos/project"
|
||||
|
||||
// We can use a regex for these, but that might have inferior performance
|
||||
projectSpaceGroupsPrefix = "cernbox-project-"
|
||||
projectSpaceAdminGroupsSuffix = "-admins"
|
||||
)
|
||||
|
||||
type wrapper struct {
|
||||
storage.FS
|
||||
conf *eosfs.Config
|
||||
mountIDTemplate *template.Template
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
|
||||
c := &eosfs.Config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
err = errors.Wrap(err, "error decoding conf")
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// default to version invariance if not configured
|
||||
if _, ok := m["version_invariant"]; !ok {
|
||||
c.VersionInvariant = true
|
||||
}
|
||||
|
||||
// allow recycle operations for project spaces
|
||||
if !c.EnableHome && strings.HasPrefix(c.Namespace, eosProjectsNamespace) {
|
||||
c.AllowPathRecycleOperations = true
|
||||
c.ImpersonateOwnerforRevisions = true
|
||||
}
|
||||
|
||||
t, ok := m["mount_id_template"].(string)
|
||||
if !ok || t == "" {
|
||||
t = "eoshome-{{ trimAll \"/\" .Path | substr 0 1 }}"
|
||||
}
|
||||
|
||||
return c, t, nil
|
||||
}
|
||||
|
||||
// New returns an implementation of the storage.FS interface that forms a wrapper
|
||||
// around separate connections to EOS.
|
||||
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
|
||||
c, t, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eos, err := eosfs.NewEOSFS(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wrapper{FS: eos, conf: c, mountIDTemplate: mountIDTemplate}, nil
|
||||
}
|
||||
|
||||
// We need to override the methods, GetMD, GetPathByID and ListFolder to fill the
|
||||
// StorageId in the ResourceInfo objects.
|
||||
|
||||
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We need to extract the mount ID based on the mapping template.
|
||||
//
|
||||
// Take the first letter of the resource path after the namespace has been removed.
|
||||
// If it's empty, leave it empty to be filled by storageprovider.
|
||||
res.Id.StorageId = w.getMountID(ctx, res)
|
||||
|
||||
if err = w.setProjectSharingPermissions(ctx, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
res.Path = path.Base(res.Path)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
|
||||
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range res {
|
||||
r.Id.StorageId = w.getMountID(ctx, r)
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
r.Path = path.Base(r.Path)
|
||||
}
|
||||
|
||||
if err = w.setProjectSharingPermissions(ctx, r); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (w *wrapper) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
|
||||
res, err := w.FS.ListRecycle(ctx, ref, key, relativePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request contains a relative reference, we also need to return the base path instead of the full one
|
||||
if utils.IsRelativeReference(ref) {
|
||||
for _, info := range res {
|
||||
info.Ref.Path = path.Base(info.Ref.Path)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (w *wrapper) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
|
||||
res, err := w.FS.ListStorageSpaces(ctx, filter, unrestricted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range res {
|
||||
if mountID, _, _, _ := storagespace.SplitID(r.Id.OpaqueId); mountID == "" {
|
||||
mountID = w.getMountID(ctx, &provider.ResourceInfo{Path: r.Name})
|
||||
r.Root.StorageId = mountID
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
func (w *wrapper) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w.FS.ListRevisions(ctx, ref)
|
||||
}
|
||||
|
||||
func (w *wrapper) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderfunc func(*provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return w.FS.DownloadRevision(ctx, ref, revisionKey, openReaderfunc)
|
||||
}
|
||||
|
||||
func (w *wrapper) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return w.FS.RestoreRevision(ctx, ref, revisionKey)
|
||||
}
|
||||
|
||||
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
|
||||
// This is only allowed for project space admins
|
||||
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.FS.DenyGrant(ctx, ref, g)
|
||||
}
|
||||
|
||||
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
|
||||
}
|
||||
|
||||
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
r.Path = strings.TrimPrefix(r.Path, w.conf.MountPath)
|
||||
b := bytes.Buffer{}
|
||||
if err := w.mountIDTemplate.Execute(&b, r); err != nil {
|
||||
return ""
|
||||
}
|
||||
r.Path = path.Join(w.conf.MountPath, r.Path)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (w *wrapper) setProjectSharingPermissions(ctx context.Context, r *provider.ResourceInfo) error {
|
||||
// Check if this storage provider corresponds to a project spaces instance
|
||||
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
|
||||
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
|
||||
parts := strings.SplitN(r.Path, "/", 4)
|
||||
if len(parts) != 4 && len(parts) != 3 {
|
||||
// The request might be for / or /$letter
|
||||
// Nothing to do in that case
|
||||
return nil
|
||||
}
|
||||
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
for _, g := range user.Groups {
|
||||
if g == adminGroup {
|
||||
r.PermissionSet.AddGrant = true
|
||||
r.PermissionSet.RemoveGrant = true
|
||||
r.PermissionSet.UpdateGrant = true
|
||||
r.PermissionSet.ListGrants = true
|
||||
r.PermissionSet.GetQuota = true
|
||||
r.PermissionSet.DenyGrant = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wrapper) userIsProjectAdmin(ctx context.Context, ref *provider.Reference) error {
|
||||
// Check if this storage provider corresponds to a project spaces instance
|
||||
if !strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
|
||||
return nil
|
||||
}
|
||||
|
||||
res, err := w.FS.GetMD(ctx, ref, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
|
||||
parts := strings.SplitN(res.Path, "/", 4)
|
||||
if len(parts) != 4 && len(parts) != 3 {
|
||||
// The request might be for / or /$letter
|
||||
// Nothing to do in that case
|
||||
return nil
|
||||
}
|
||||
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
for _, g := range user.Groups {
|
||||
if g == adminGroup {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errtypes.PermissionDenied("eosfs: project spaces revisions can only be accessed by admins")
|
||||
}
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
// 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 rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const (
|
||||
userPrefix = "user:"
|
||||
usernamePrefix = "username:"
|
||||
userIDPrefix = "userid:"
|
||||
namePrefix = "name:"
|
||||
mailPrefix = "mail:"
|
||||
uidPrefix = "uid:"
|
||||
userGroupsPrefix = "groups:"
|
||||
)
|
||||
|
||||
func initRedisPool(address, username, password string) *redis.Pool {
|
||||
return &redis.Pool{
|
||||
|
||||
MaxIdle: 50,
|
||||
MaxActive: 1000,
|
||||
IdleTimeout: 240 * time.Second,
|
||||
|
||||
Dial: func() (redis.Conn, error) {
|
||||
var opts []redis.DialOption
|
||||
if username != "" {
|
||||
opts = append(opts, redis.DialUsername(username))
|
||||
}
|
||||
if password != "" {
|
||||
opts = append(opts, redis.DialPassword(password))
|
||||
}
|
||||
|
||||
c, err := redis.Dial("tcp", address, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) setVal(key, val string, expiration int) error {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
args := []interface{}{key, val}
|
||||
if expiration != -1 {
|
||||
args = append(args, "EX", expiration)
|
||||
}
|
||||
if _, err := conn.Do("SET", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) getVal(key string) (string, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
val, err := redis.String(conn.Do("GET", key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) findCachedUsers(query string) ([]*userpb.User, error) {
|
||||
conn := m.redisPool.Get()
|
||||
defer conn.Close()
|
||||
if conn != nil {
|
||||
query = fmt.Sprintf("%s*%s*", userPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
|
||||
keys, err := redis.Strings(conn.Do("KEYS", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var args []interface{}
|
||||
for _, k := range keys {
|
||||
args = append(args, k)
|
||||
}
|
||||
|
||||
// Fetch the users for all these keys
|
||||
userStrings, err := redis.Strings(conn.Do("MGET", args...))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userMap := make(map[string]*userpb.User)
|
||||
for _, user := range userStrings {
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err == nil {
|
||||
userMap[u.Id.OpaqueId] = &u
|
||||
}
|
||||
}
|
||||
|
||||
var users []*userpb.User
|
||||
for _, u := range userMap {
|
||||
users = append(users, u)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("rest: unable to get connection from redis pool")
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserDetails(uid *userpb.UserId) (*userpb.User, error) {
|
||||
user, err := m.getVal(userPrefix + usernamePrefix + strings.ToLower(uid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheUserDetails(u *userpb.User) error {
|
||||
encodedUser, err := json.Marshal(&u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(userPrefix+usernamePrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = m.setVal(userPrefix+userIDPrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if u.Mail != "" {
|
||||
if err = m.setVal(userPrefix+mailPrefix+strings.ToLower(u.Mail), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if u.DisplayName != "" {
|
||||
if err = m.setVal(userPrefix+namePrefix+u.Id.OpaqueId+"_"+strings.ReplaceAll(strings.ToLower(u.DisplayName), " ", "_"), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if u.UidNumber != 0 {
|
||||
if err = m.setVal(userPrefix+uidPrefix+strconv.FormatInt(u.UidNumber, 10), string(encodedUser), -1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserByParam(field, claim string) (*userpb.User, error) {
|
||||
user, err := m.getVal(userPrefix + field + ":" + strings.ToLower(claim))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := userpb.User{}
|
||||
if err = json.Unmarshal([]byte(user), &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchCachedUserGroups(uid *userpb.UserId) ([]string, error) {
|
||||
groups, err := m.getVal(userPrefix + userGroupsPrefix + strings.ToLower(uid.OpaqueId))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := []string{}
|
||||
if err = json.Unmarshal([]byte(groups), &g); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (m *manager) cacheUserGroups(uid *userpb.UserId, groups []string) error {
|
||||
g, err := json.Marshal(&groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.setVal(userPrefix+userGroupsPrefix+strings.ToLower(uid.OpaqueId), string(g), m.conf.UserGroupsCacheExpiration*60)
|
||||
}
|
||||
-391
@@ -1,391 +0,0 @@
|
||||
// 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("rest", New)
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
redisPool *redis.Pool
|
||||
apiTokenManager *utils.APITokenManager
|
||||
}
|
||||
|
||||
type config struct {
|
||||
// The address at which the redis server is running
|
||||
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
|
||||
// The username for connecting to the redis server
|
||||
RedisUsername string `mapstructure:"redis_username" docs:""`
|
||||
// The password for connecting to the redis server
|
||||
RedisPassword string `mapstructure:"redis_password" docs:""`
|
||||
// The time in minutes for which the groups to which a user belongs would be cached
|
||||
UserGroupsCacheExpiration int `mapstructure:"user_groups_cache_expiration" docs:"5"`
|
||||
// The OIDC Provider
|
||||
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
|
||||
// Base API Endpoint
|
||||
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
|
||||
// Client ID needed to authenticate
|
||||
ClientID string `mapstructure:"client_id" docs:"-"`
|
||||
// Client Secret
|
||||
ClientSecret string `mapstructure:"client_secret" docs:"-"`
|
||||
|
||||
// Endpoint to generate token to access the API
|
||||
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
|
||||
// The target application for which token needs to be generated
|
||||
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
|
||||
// The time in seconds between bulk fetch of user accounts
|
||||
UserFetchInterval int `mapstructure:"user_fetch_interval" docs:"3600"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.UserGroupsCacheExpiration == 0 {
|
||||
c.UserGroupsCacheExpiration = 5
|
||||
}
|
||||
if c.RedisAddress == "" {
|
||||
c.RedisAddress = ":6379"
|
||||
}
|
||||
if c.APIBaseURL == "" {
|
||||
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
|
||||
}
|
||||
if c.TargetAPI == "" {
|
||||
c.TargetAPI = "authorization-service-api"
|
||||
}
|
||||
if c.OIDCTokenEndpoint == "" {
|
||||
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
|
||||
}
|
||||
if c.IDProvider == "" {
|
||||
c.IDProvider = "http://cernbox.cern.ch"
|
||||
}
|
||||
if c.UserFetchInterval == 0 {
|
||||
c.UserFetchInterval = 3600
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// New returns a user manager implementation that makes calls to the GRAPPA API.
|
||||
func New(m map[string]interface{}) (user.Manager, error) {
|
||||
mgr := &manager{}
|
||||
err := mgr.Configure(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mgr, err
|
||||
}
|
||||
|
||||
func (m *manager) Configure(ml map[string]interface{}) error {
|
||||
c, err := parseConfig(ml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.init()
|
||||
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
|
||||
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
|
||||
m.conf = c
|
||||
m.redisPool = redisPool
|
||||
m.apiTokenManager = apiTokenManager
|
||||
|
||||
// Since we're starting a subroutine which would take some time to execute,
|
||||
// we can't wait to see if it works before returning the user.Manager object
|
||||
// TODO: return err if the fetch fails
|
||||
go m.fetchAllUsers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllUsers() {
|
||||
_ = m.fetchAllUserAccounts()
|
||||
ticker := time.NewTicker(time.Duration(m.conf.UserFetchInterval) * time.Second)
|
||||
work := make(chan os.Signal, 1)
|
||||
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-work:
|
||||
return
|
||||
case <-ticker.C:
|
||||
_ = m.fetchAllUserAccounts()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manager) fetchAllUserAccounts() error {
|
||||
ctx := context.Background()
|
||||
url := fmt.Sprintf("%s/api/v1.0/Identity?field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", m.conf.APIBaseURL)
|
||||
|
||||
for url != "" {
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
responseData, ok := result["data"].([]interface{})
|
||||
if !ok {
|
||||
return errors.New("rest: error in type assertion")
|
||||
}
|
||||
for _, usr := range responseData {
|
||||
userData, ok := usr.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = m.parseAndCacheUser(ctx, userData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
url = ""
|
||||
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
|
||||
if links, ok := pagination["links"].(map[string]interface{}); ok {
|
||||
if next, ok := links["next"].(string); ok {
|
||||
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *manager) parseAndCacheUser(ctx context.Context, userData map[string]interface{}) (*userpb.User, error) {
|
||||
upn, ok := userData["upn"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("rest: missing upn in user data")
|
||||
}
|
||||
mail, _ := userData["primaryAccountEmail"].(string)
|
||||
name, _ := userData["displayName"].(string)
|
||||
uidNumber, _ := userData["uid"].(float64)
|
||||
gidNumber, _ := userData["gid"].(float64)
|
||||
t, _ := userData["type"].(string)
|
||||
userType := getUserType(t, upn)
|
||||
|
||||
userID := &userpb.UserId{
|
||||
OpaqueId: upn,
|
||||
Idp: m.conf.IDProvider,
|
||||
Type: userType,
|
||||
}
|
||||
u := &userpb.User{
|
||||
Id: userID,
|
||||
Username: upn,
|
||||
Mail: mail,
|
||||
DisplayName: name,
|
||||
UidNumber: int64(uidNumber),
|
||||
GidNumber: int64(gidNumber),
|
||||
}
|
||||
|
||||
if err := m.cacheUserDetails(u); err != nil {
|
||||
log.Error().Err(err).Msg("rest: error caching user details")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if uid.GetTenantId() != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
u, err := m.fetchCachedUserDetails(uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingGroups {
|
||||
userGroups, err := m.GetUserGroups(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Groups = userGroups
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetUserByClaim(ctx context.Context, claim, value, tenantID string, skipFetchingGroups bool) (*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
u, err := m.fetchCachedUserByParam(claim, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipFetchingGroups {
|
||||
userGroups, err := m.GetUserGroups(ctx, u.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u.Groups = userGroups
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (m *manager) FindUsers(ctx context.Context, query, tenantID string, skipFetchingGroups bool) ([]*userpb.User, error) {
|
||||
if tenantID != "" {
|
||||
return nil, errtypes.NotSupported("tenant filter not supported in rest user manager")
|
||||
}
|
||||
|
||||
// Look at namespaces filters. If the query starts with:
|
||||
// "a" => look into primary/secondary/service accounts
|
||||
// "l" => look into lightweight/federated accounts
|
||||
// none => look into primary
|
||||
|
||||
parts := strings.SplitN(query, ":", 2)
|
||||
|
||||
var namespace string
|
||||
if len(parts) == 2 {
|
||||
// the query contains a namespace filter
|
||||
namespace, query = parts[0], parts[1]
|
||||
}
|
||||
|
||||
users, err := m.findCachedUsers(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userSlice := []*userpb.User{}
|
||||
|
||||
var accountsFilters []userpb.UserType
|
||||
switch namespace {
|
||||
case "":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY}
|
||||
case "a":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY, userpb.UserType_USER_TYPE_SECONDARY, userpb.UserType_USER_TYPE_SERVICE}
|
||||
case "l":
|
||||
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_LIGHTWEIGHT, userpb.UserType_USER_TYPE_FEDERATED}
|
||||
}
|
||||
|
||||
for _, u := range users {
|
||||
if isUserAnyType(u, accountsFilters) {
|
||||
userSlice = append(userSlice, u)
|
||||
}
|
||||
}
|
||||
|
||||
return userSlice, nil
|
||||
}
|
||||
|
||||
// isUserAnyType returns true if the user's type is one of types list
|
||||
func isUserAnyType(user *userpb.User, types []userpb.UserType) bool {
|
||||
for _, t := range types {
|
||||
if user.GetId().Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
|
||||
groups, err := m.fetchCachedUserGroups(uid)
|
||||
if err == nil {
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/api/v1.0/Identity/%s/groups?recursive=true", m.conf.APIBaseURL, uid.OpaqueId)
|
||||
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupData := result["data"].([]interface{})
|
||||
groups = []string{}
|
||||
|
||||
for _, g := range groupData {
|
||||
groupInfo, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("rest: error in type assertion")
|
||||
}
|
||||
name, ok := groupInfo["displayName"].(string)
|
||||
if ok {
|
||||
groups = append(groups, name)
|
||||
}
|
||||
}
|
||||
|
||||
if err = m.cacheUserGroups(uid, groups); err != nil {
|
||||
log := appctx.GetLogger(ctx)
|
||||
log.Error().Err(err).Msg("rest: error caching user groups")
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) {
|
||||
userGroups, err := m.GetUserGroups(ctx, uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, g := range userGroups {
|
||||
if group == g {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func getUserType(userType, upn string) userpb.UserType {
|
||||
var t userpb.UserType
|
||||
switch userType {
|
||||
case "Application":
|
||||
t = userpb.UserType_USER_TYPE_APPLICATION
|
||||
case "Service":
|
||||
t = userpb.UserType_USER_TYPE_SERVICE
|
||||
case "Secondary":
|
||||
t = userpb.UserType_USER_TYPE_SECONDARY
|
||||
case "Person":
|
||||
switch {
|
||||
case strings.HasPrefix(upn, "guest"):
|
||||
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
|
||||
case strings.Contains(upn, "@"):
|
||||
t = userpb.UserType_USER_TYPE_FEDERATED
|
||||
default:
|
||||
t = userpb.UserType_USER_TYPE_PRIMARY
|
||||
}
|
||||
default:
|
||||
t = userpb.UserType_USER_TYPE_INVALID
|
||||
}
|
||||
return t
|
||||
|
||||
}
|
||||
-308
@@ -1,308 +0,0 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "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"
|
||||
rpcv1beta1 "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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
)
|
||||
|
||||
// DBShare stores information about user and public shares.
|
||||
type DBShare struct {
|
||||
ID string
|
||||
UIDOwner string
|
||||
UIDInitiator string
|
||||
Prefix string
|
||||
ItemSource string
|
||||
ItemType string
|
||||
ShareWith string
|
||||
Token string
|
||||
Expiration string
|
||||
Permissions int
|
||||
ShareType int
|
||||
ShareName string
|
||||
STime int
|
||||
FileTarget string
|
||||
State int
|
||||
Quicklink bool
|
||||
Description string
|
||||
NotifyUploads bool
|
||||
NotifyUploadsExtraRecipients string
|
||||
}
|
||||
|
||||
// FormatGrantee formats a CS3API grantee to a string.
|
||||
func FormatGrantee(g *provider.Grantee) (int, string) {
|
||||
var granteeType int
|
||||
var formattedID string
|
||||
switch g.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
granteeType = 0
|
||||
formattedID = FormatUserID(g.GetUserId())
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
granteeType = 1
|
||||
formattedID = FormatGroupID(g.GetGroupId())
|
||||
default:
|
||||
granteeType = -1
|
||||
}
|
||||
return granteeType, formattedID
|
||||
}
|
||||
|
||||
// ExtractGrantee retrieves the CS3API grantee from a formatted string.
|
||||
func ExtractGrantee(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, t int, g string) (*provider.Grantee, error) {
|
||||
var grantee provider.Grantee
|
||||
switch t {
|
||||
case 0:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
|
||||
user, err := ExtractUserID(ctx, gateway, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Id = &provider.Grantee_UserId{UserId: user}
|
||||
case 1:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
|
||||
group, err := ExtractGroupID(ctx, gateway, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Id = &provider.Grantee_GroupId{GroupId: group}
|
||||
default:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
|
||||
}
|
||||
return &grantee, nil
|
||||
}
|
||||
|
||||
// ResourceTypeToItem maps a resource type to a string.
|
||||
func ResourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceTypeToItemInt maps a resource type to an integer.
|
||||
func ResourceTypeToItemInt(r provider.ResourceType) int {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return 0
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return 1
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
// SharePermToInt maps read/write permissions to an integer.
|
||||
func SharePermToInt(p *provider.ResourcePermissions) int {
|
||||
var perm int
|
||||
switch {
|
||||
case p.InitiateFileUpload && !p.InitiateFileDownload:
|
||||
perm = 4
|
||||
case p.InitiateFileUpload:
|
||||
perm = 15
|
||||
case p.InitiateFileDownload:
|
||||
perm = 1
|
||||
}
|
||||
// TODO map denials and resharing; currently, denials are mapped to 0
|
||||
return perm
|
||||
}
|
||||
|
||||
// IntTosharePerm retrieves read/write permissions from an integer.
|
||||
func IntTosharePerm(p int, itemType string) *provider.ResourcePermissions {
|
||||
switch p {
|
||||
case 1:
|
||||
return conversions.NewViewerRole().CS3ResourcePermissions()
|
||||
case 15:
|
||||
if itemType == "folder" {
|
||||
return conversions.NewEditorRole().CS3ResourcePermissions()
|
||||
}
|
||||
return conversions.NewFileEditorRole().CS3ResourcePermissions()
|
||||
case 4:
|
||||
return conversions.NewUploaderRole().CS3ResourcePermissions()
|
||||
default:
|
||||
// TODO we may have other options, for now this is a denial
|
||||
return &provider.ResourcePermissions{}
|
||||
}
|
||||
}
|
||||
|
||||
// IntToShareState retrieves the received share state from an integer.
|
||||
func IntToShareState(g int) collaboration.ShareState {
|
||||
switch g {
|
||||
case 0:
|
||||
return collaboration.ShareState_SHARE_STATE_PENDING
|
||||
case 1:
|
||||
return collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
case -1:
|
||||
return collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
default:
|
||||
return collaboration.ShareState_SHARE_STATE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
// FormatUserID formats a CS3API user ID to a string.
|
||||
func FormatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ExtractUserID retrieves a CS3API user ID from a string.
|
||||
func ExtractUserID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*userpb.UserId, error) {
|
||||
userRes, err := gateway.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: &userpb.UserId{OpaqueId: u},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
return nil, errors.New(userRes.Status.Message)
|
||||
}
|
||||
|
||||
return userRes.User.Id, nil
|
||||
}
|
||||
|
||||
// FormatGroupID formats a CS3API group ID to a string.
|
||||
func FormatGroupID(u *grouppb.GroupId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ExtractGroupID retrieves a CS3API group ID from a string.
|
||||
func ExtractGroupID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*grouppb.GroupId, error) {
|
||||
groupRes, err := gateway.GetGroup(ctx, &grouppb.GetGroupRequest{
|
||||
GroupId: &grouppb.GroupId{OpaqueId: u},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
return nil, errors.New(groupRes.Status.Message)
|
||||
}
|
||||
return groupRes.Group.Id, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share.
|
||||
func ConvertToCS3Share(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.Share, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee, err := ExtractGrantee(ctx, gateway, s.ShareType, s.ShareWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
//ResourceId: &provider.Reference{StorageId: s.Prefix, NodeId: s.ItemSource},
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: s.Prefix,
|
||||
OpaqueId: s.ItemSource,
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
|
||||
Grantee: grantee,
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share.
|
||||
func ConvertToCS3ReceivedShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.ReceivedShare, error) {
|
||||
share, err := ConvertToCS3Share(ctx, gateway, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &collaboration.ReceivedShare{
|
||||
Share: share,
|
||||
State: IntToShareState(s.State),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share.
|
||||
func ConvertToCS3PublicShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*link.PublicShare, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
pwd := s.ShareWith != ""
|
||||
var expires *typespb.Timestamp
|
||||
if s.Expiration != "" {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
|
||||
if err == nil {
|
||||
expires = &typespb.Timestamp{
|
||||
Seconds: uint64(t.Unix()),
|
||||
}
|
||||
}
|
||||
}
|
||||
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
StorageId: s.Prefix,
|
||||
OpaqueId: s.ItemSource,
|
||||
},
|
||||
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Token: s.Token,
|
||||
DisplayName: s.ShareName,
|
||||
PasswordProtected: pwd,
|
||||
Expiration: expires,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
Quicklink: s.Quicklink,
|
||||
Description: s.Description,
|
||||
NotifyUploads: s.NotifyUploads,
|
||||
NotifyUploadsExtraRecipients: s.NotifyUploadsExtraRecipients,
|
||||
}, nil
|
||||
}
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
|
||||
)
|
||||
|
||||
// APITokenManager stores config related to api management
|
||||
type APITokenManager struct {
|
||||
oidcToken OIDCToken
|
||||
conf *config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// OIDCToken stores the OIDC token used to authenticate requests to the REST API service
|
||||
type OIDCToken struct {
|
||||
sync.Mutex // concurrent access to apiToken and tokenExpirationTime
|
||||
apiToken string
|
||||
tokenExpirationTime time.Time
|
||||
}
|
||||
|
||||
type config struct {
|
||||
TargetAPI string
|
||||
OIDCTokenEndpoint string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
// InitAPITokenManager initializes a new APITokenManager
|
||||
func InitAPITokenManager(targetAPI, oidcTokenEndpoint, clientID, clientSecret string) *APITokenManager {
|
||||
return &APITokenManager{
|
||||
conf: &config{
|
||||
TargetAPI: targetAPI,
|
||||
OIDCTokenEndpoint: oidcTokenEndpoint,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
client: rhttp.GetHTTPClient(
|
||||
rhttp.Timeout(10*time.Second),
|
||||
rhttp.Insecure(true),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *APITokenManager) renewAPIToken(ctx context.Context, forceRenewal bool) error {
|
||||
// Received tokens have an expiration time of 20 minutes.
|
||||
// Take a couple of seconds as buffer time for the API call to complete
|
||||
if forceRenewal || a.oidcToken.tokenExpirationTime.Before(time.Now().Add(time.Second*time.Duration(2))) {
|
||||
token, expiration, err := a.getAPIToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.oidcToken.Lock()
|
||||
defer a.oidcToken.Unlock()
|
||||
|
||||
a.oidcToken.apiToken = token
|
||||
a.oidcToken.tokenExpirationTime = expiration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *APITokenManager) getAPIToken(ctx context.Context) (string, time.Time, error) {
|
||||
|
||||
params := url.Values{
|
||||
"grant_type": {"client_credentials"},
|
||||
"audience": {a.conf.TargetAPI},
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", a.conf.OIDCTokenEndpoint, strings.NewReader(params.Encode()))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
httpReq.SetBasicAuth(a.conf.ClientID, a.conf.ClientSecret)
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
|
||||
httpRes, err := a.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(httpRes.Body)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
|
||||
return "", time.Time{}, errors.New("rest: get token endpoint returned " + httpRes.Status)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
expirationSecs := result["expires_in"].(float64)
|
||||
expirationTime := time.Now().Add(time.Second * time.Duration(expirationSecs))
|
||||
return result["access_token"].(string), expirationTime, nil
|
||||
}
|
||||
|
||||
// SendAPIGetRequest makes an API GET Request to the passed URL
|
||||
func (a *APITokenManager) SendAPIGetRequest(ctx context.Context, url string, forceRenewal bool) (map[string]interface{}, error) {
|
||||
err := a.renewAPIToken(ctx, forceRenewal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We don't need to take the lock when reading apiToken, because if we reach here,
|
||||
// the token is valid at least for a couple of seconds. Even if another request modifies
|
||||
// the token and expiration time while this request is in progress, the current token will still be valid.
|
||||
httpReq.Header.Set("Authorization", "Bearer "+a.oidcToken.apiToken)
|
||||
|
||||
httpRes, err := a.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer httpRes.Body.Close()
|
||||
|
||||
if httpRes.StatusCode == http.StatusUnauthorized {
|
||||
// The token is no longer valid, try renewing it
|
||||
return a.SendAPIGetRequest(ctx, url, true)
|
||||
}
|
||||
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
|
||||
return nil, errors.New("rest: API request returned " + httpRes.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(httpRes.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(body, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 OpenCloud GmbH <mail@opencloud.eu>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
)
|
||||
|
||||
// FavoriteAdded is emitted when a user added a resource to their favorites
|
||||
type FavoriteAdded struct {
|
||||
Ref *provider.Reference
|
||||
Executant *user.UserId
|
||||
UserID *user.UserId
|
||||
Timestamp *types.Timestamp
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
func (FavoriteAdded) Unmarshal(v []byte) (interface{}, error) {
|
||||
e := FavoriteAdded{}
|
||||
err := json.Unmarshal(v, &e)
|
||||
return e, err
|
||||
}
|
||||
|
||||
// FavoriteRemoved is emitted when a user removed a resource from their favorites
|
||||
type FavoriteRemoved struct {
|
||||
Ref *provider.Reference
|
||||
Executant *user.UserId
|
||||
UserID *user.UserId
|
||||
Timestamp *types.Timestamp
|
||||
}
|
||||
|
||||
// Unmarshal to fulfill umarshaller interface
|
||||
func (FavoriteRemoved) Unmarshal(v []byte) (interface{}, error) {
|
||||
e := FavoriteRemoved{}
|
||||
err := json.Unmarshal(v, &e)
|
||||
return e, err
|
||||
}
|
||||
Generated
Vendored
-1
@@ -22,6 +22,5 @@ import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/sql"
|
||||
// Add your own here.
|
||||
)
|
||||
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
// 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 sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gatewayv1beta1 "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"
|
||||
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/go-sql-driver/mysql"
|
||||
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/ocm/invite/repository/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This module implement the invite.Repository interface as a mysql driver.
|
||||
//
|
||||
// The OCM Invitation tokens are saved in the table:
|
||||
// ocm_tokens(*token*, initiator, expiration, description)
|
||||
//
|
||||
// The OCM remote user are saved in the table:
|
||||
// ocm_remote_users(*initiator*, *opaque_user_id*, *idp*, email, display_name)
|
||||
|
||||
func init() {
|
||||
registry.Register("sql", New)
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
db *sql.DB
|
||||
client gatewayv1beta1.GatewayAPIClient
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DBUsername string `mapstructure:"db_username"`
|
||||
DBPassword string `mapstructure:"db_password"`
|
||||
DBAddress string `mapstructure:"db_address"`
|
||||
DBName string `mapstructure:"db_name"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
}
|
||||
|
||||
func (c *config) ApplyDefaults() {
|
||||
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
|
||||
}
|
||||
|
||||
// New creates a sql repository for ocm tokens and users.
|
||||
func New(m map[string]interface{}) (invite.Repository, error) {
|
||||
var c config
|
||||
if err := cfg.Decode(m, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", c.DBUsername, c.DBPassword, c.DBAddress, c.DBName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sql: error opening connection to mysql database")
|
||||
}
|
||||
|
||||
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mgr := mgr{
|
||||
c: &c,
|
||||
db: db,
|
||||
client: gw,
|
||||
}
|
||||
return &mgr, nil
|
||||
}
|
||||
|
||||
// AddToken stores the token in the repository.
|
||||
func (m *mgr) AddToken(ctx context.Context, token *invitepb.InviteToken) error {
|
||||
query := "INSERT INTO ocm_tokens SET token=?,initiator=?,expiration=?,description=?"
|
||||
_, err := m.db.ExecContext(ctx, query, token.Token, conversions.FormatUserID(token.UserId), timestampToTime(token.Expiration), token.Description)
|
||||
return err
|
||||
}
|
||||
|
||||
func timestampToTime(t *types.Timestamp) time.Time {
|
||||
return time.Unix(int64(t.Seconds), int64(t.Nanos))
|
||||
}
|
||||
|
||||
type dbToken struct {
|
||||
Token string
|
||||
Initiator string
|
||||
Expiration time.Time
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetToken gets the token from the repository.
|
||||
func (m *mgr) GetToken(ctx context.Context, token string) (*invitepb.InviteToken, error) {
|
||||
query := "SELECT token, initiator, expiration, description FROM ocm_tokens where token=?"
|
||||
|
||||
var tkn dbToken
|
||||
if err := m.db.QueryRowContext(ctx, query, token).Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, invite.ErrTokenNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToInviteToken(ctx, tkn)
|
||||
}
|
||||
|
||||
func (m *mgr) convertToInviteToken(ctx context.Context, tkn dbToken) (*invitepb.InviteToken, error) {
|
||||
user, err := conversions.ExtractUserID(ctx, m.client, tkn.Initiator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &invitepb.InviteToken{
|
||||
Token: tkn.Token,
|
||||
UserId: user,
|
||||
Expiration: &types.Timestamp{
|
||||
Seconds: uint64(tkn.Expiration.Unix()),
|
||||
},
|
||||
Description: tkn.Description,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListTokens(ctx context.Context, initiator *userpb.UserId) ([]*invitepb.InviteToken, error) {
|
||||
query := "SELECT token, initiator, expiration, description FROM ocm_tokens WHERE initiator=? AND expiration > NOW()"
|
||||
|
||||
tokens := []*invitepb.InviteToken{}
|
||||
rows, err := m.db.QueryContext(ctx, query, conversions.FormatUserID(initiator))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tkn dbToken
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&tkn.Token, &tkn.Initiator, &tkn.Expiration, &tkn.Description); err != nil {
|
||||
continue
|
||||
}
|
||||
token, err := m.convertToInviteToken(ctx, tkn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// AddRemoteUser stores the remote user.
|
||||
func (m *mgr) AddRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.User) error {
|
||||
query := "INSERT INTO ocm_remote_users SET initiator=?, opaque_user_id=?, idp=?, email=?, display_name=?"
|
||||
if _, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser.Id), remoteUser.Id.Idp, remoteUser.Mail, remoteUser.DisplayName); err != nil {
|
||||
// check if the user already exist in the db
|
||||
// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry
|
||||
var e *mysql.MySQLError
|
||||
if errors.As(err, &e) && e.Number == 1062 {
|
||||
return invite.ErrUserAlreadyAccepted
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbOCMUser struct {
|
||||
OpaqueUserID string
|
||||
Idp string
|
||||
Email string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
// GetRemoteUser retrieves details about a remote user who has accepted an invite to share.
|
||||
func (m *mgr) GetRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUserID *userpb.UserId) (*userpb.User, error) {
|
||||
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
|
||||
|
||||
var user dbOCMUser
|
||||
if err := m.db.QueryRowContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUserID), remoteUserID.Idp).
|
||||
Scan(&user.OpaqueUserID, &user.Idp, &user.Email, &user.DisplayName); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, errtypes.NotFound(remoteUserID.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return user.toCS3User(), nil
|
||||
}
|
||||
|
||||
func (u *dbOCMUser) toCS3User() *userpb.User {
|
||||
return &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
Idp: u.Idp,
|
||||
OpaqueId: u.OpaqueUserID,
|
||||
Type: userpb.UserType_USER_TYPE_FEDERATED,
|
||||
},
|
||||
Mail: u.Email,
|
||||
DisplayName: u.DisplayName,
|
||||
}
|
||||
}
|
||||
|
||||
// FindRemoteUsers finds remote users who have accepted invites based on their attributes.
|
||||
func (m *mgr) FindRemoteUsers(ctx context.Context, initiator *userpb.UserId, attr string) ([]*userpb.User, error) {
|
||||
// TODO: (gdelmont) this query can get really slow in case the number of rows is too high.
|
||||
// For the time being this is not expected, but if in future this happens, consider to add
|
||||
// a fulltext index.
|
||||
query := "SELECT opaque_user_id, idp, email, display_name FROM ocm_remote_users WHERE initiator=? AND (opaque_user_id LIKE ? OR idp LIKE ? OR email LIKE ? OR display_name LIKE ?)"
|
||||
s := "%" + attr + "%"
|
||||
params := []any{conversions.FormatUserID(initiator), s, s, s, s}
|
||||
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var u dbOCMUser
|
||||
var users []*userpb.User
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&u.OpaqueUserID, &u.Idp, &u.Email, &u.DisplayName); err != nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, u.toCS3User())
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (m *mgr) DeleteRemoteUser(ctx context.Context, initiator *userpb.UserId, remoteUser *userpb.UserId) error {
|
||||
query := "DELETE FROM ocm_remote_users WHERE initiator=? AND opaque_user_id=? AND idp=?"
|
||||
_, err := m.db.ExecContext(ctx, query, conversions.FormatUserID(initiator), conversions.FormatUserID(remoteUser), remoteUser.Idp)
|
||||
return err
|
||||
}
|
||||
+8
@@ -609,3 +609,11 @@ func (d *driver) UpdateStorageSpace(ctx context.Context, req *provider.UpdateSto
|
||||
func (d *driver) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) error {
|
||||
return errtypes.NotSupported("operation not supported")
|
||||
}
|
||||
|
||||
func (d *driver) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (d *driver) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
-1
@@ -22,6 +22,5 @@ import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/owncloudsql"
|
||||
// Add your own here
|
||||
)
|
||||
|
||||
Generated
Vendored
-223
@@ -1,223 +0,0 @@
|
||||
// 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 owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
)
|
||||
|
||||
// DBShare stores information about user and public shares.
|
||||
type DBShare struct {
|
||||
ID string
|
||||
UIDOwner string
|
||||
UIDInitiator string
|
||||
ItemStorage string
|
||||
FileSource string
|
||||
ItemType string // 'file' or 'folder'
|
||||
ShareWith string
|
||||
Token string
|
||||
Expiration string
|
||||
Permissions int
|
||||
ShareType int
|
||||
ShareName string
|
||||
STime int
|
||||
FileTarget string
|
||||
RejectedBy string
|
||||
State int
|
||||
Parent int
|
||||
}
|
||||
|
||||
// UserConverter describes an interface for converting user ids to names and back
|
||||
type UserConverter interface {
|
||||
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
|
||||
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
|
||||
}
|
||||
|
||||
// GatewayUserConverter converts usernames and ids using the gateway
|
||||
type GatewayUserConverter struct {
|
||||
gwAddr string
|
||||
|
||||
IDCache *ttlcache.Cache
|
||||
NameCache *ttlcache.Cache
|
||||
}
|
||||
|
||||
// NewGatewayUserConverter returns a instance of GatewayUserConverter
|
||||
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
|
||||
IDCache := ttlcache.NewCache()
|
||||
_ = IDCache.SetTTL(30 * time.Second)
|
||||
IDCache.SkipTTLExtensionOnHit(true)
|
||||
NameCache := ttlcache.NewCache()
|
||||
_ = NameCache.SetTTL(30 * time.Second)
|
||||
NameCache.SkipTTLExtensionOnHit(true)
|
||||
|
||||
return &GatewayUserConverter{
|
||||
gwAddr: gwAddr,
|
||||
IDCache: IDCache,
|
||||
NameCache: NameCache,
|
||||
}
|
||||
}
|
||||
|
||||
// UserIDToUserName converts a user ID to an username
|
||||
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
|
||||
username, err := c.NameCache.Get(userid.String())
|
||||
if err == nil {
|
||||
return username.(string), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: userid,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
|
||||
return getUserResponse.User.Username, nil
|
||||
}
|
||||
|
||||
// UserNameToUserID converts a username to an user ID
|
||||
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
|
||||
id, err := c.IDCache.Get(username)
|
||||
if err == nil {
|
||||
return id.(*userpb.UserId), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: username,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.IDCache.Set(username, getUserResponse.User.Id)
|
||||
return getUserResponse.User.Id, nil
|
||||
}
|
||||
|
||||
func resourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sharePermToInt(p *provider.ResourcePermissions) int {
|
||||
return int(conversions.RoleFromResourcePermissions(p, true).OCSPermissions())
|
||||
}
|
||||
|
||||
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
|
||||
perms, err := conversions.NewPermissions(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
|
||||
}
|
||||
|
||||
func formatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
|
||||
func (m *mgr) ConvertToCS3PublicShare(ctx context.Context, s DBShare) (*link.PublicShare, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
permissions, err := intTosharePerm(s.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var creator *userpb.UserId
|
||||
if s.UIDOwner == s.UIDInitiator {
|
||||
creator = owner
|
||||
} else {
|
||||
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pwd := s.ShareWith != ""
|
||||
var expires *typespb.Timestamp
|
||||
if s.Expiration != "" {
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02 15:04:05-07:00", s.Expiration)
|
||||
}
|
||||
if err == nil {
|
||||
expires = &typespb.Timestamp{
|
||||
Seconds: uint64(t.Unix()),
|
||||
}
|
||||
}
|
||||
}
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: s.ItemStorage,
|
||||
OpaqueId: s.FileSource,
|
||||
},
|
||||
Permissions: &link.PublicSharePermissions{Permissions: permissions},
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Token: s.Token,
|
||||
DisplayName: s.ShareName,
|
||||
PasswordProtected: pwd,
|
||||
Expiration: expires,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
-533
@@ -1,533 +0,0 @@
|
||||
// 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 owncloudsql implements a publiclink share manager backed by an existing ownCloud 10 database
|
||||
//
|
||||
// The SQL queries use `coalesce({column_identifier}, ”) as {column_identifier}` to read an emptystring
|
||||
// instead of null values, which better fits the golang default values.
|
||||
package owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/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/publicshare"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
publicShareType = 3
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
// Config configures an owncloudsql publicshare manager
|
||||
type Config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
|
||||
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
c Config
|
||||
userConverter UserConverter
|
||||
}
|
||||
|
||||
// NewMysql returns a new publicshare manager connection to a mysql database
|
||||
func NewMysql(m map[string]interface{}) (publicshare.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConverter := NewGatewayUserConverter(sharedconf.GetGatewaySVC(c.GatewayAddr))
|
||||
|
||||
return New("mysql", db, *c, userConverter)
|
||||
}
|
||||
|
||||
// New returns a new Cache instance connecting to the given sql.DB
|
||||
func New(driver string, db *sql.DB, c Config, userConverter UserConverter) (publicshare.Manager, error) {
|
||||
if c.SharePasswordHashCost == 0 {
|
||||
c.SharePasswordHashCost = bcrypt.DefaultCost
|
||||
}
|
||||
return &mgr{
|
||||
driver: driver,
|
||||
db: db,
|
||||
c: c,
|
||||
userConverter: userConverter,
|
||||
}, 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 (m *mgr) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
|
||||
|
||||
tkn := utils.RandString(15)
|
||||
now := time.Now().Unix()
|
||||
|
||||
displayName := tkn
|
||||
if rInfo.ArbitraryMetadata != nil && rInfo.ArbitraryMetadata.Metadata["name"] != "" {
|
||||
displayName = rInfo.ArbitraryMetadata.Metadata["name"]
|
||||
}
|
||||
createdAt := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
creator := u.Username
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, rInfo.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
permissions := sharePermToInt(g.Permissions.Permissions)
|
||||
itemType := resourceTypeToItem(rInfo.Type)
|
||||
|
||||
itemSource := rInfo.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
columns := "share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,token,share_name"
|
||||
placeholders := "?,?,?,?,?,?,?,?,?,?"
|
||||
params := []interface{}{publicShareType, owner, creator, itemType, itemSource, fileSource, permissions, now, tkn, displayName}
|
||||
|
||||
var passwordProtected bool
|
||||
password := g.Password
|
||||
if password != "" {
|
||||
password, err = hashPassword(password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
passwordProtected = true
|
||||
|
||||
columns += ",share_with"
|
||||
placeholders += ",?"
|
||||
params = append(params, password)
|
||||
}
|
||||
|
||||
if g.Expiration != nil && g.Expiration.Seconds != 0 {
|
||||
t := time.Unix(int64(g.Expiration.Seconds), 0)
|
||||
columns += ",expiration"
|
||||
placeholders += ",?"
|
||||
params = append(params, t)
|
||||
}
|
||||
|
||||
query := "INSERT INTO oc_share (" + columns + ") VALUES (" + placeholders + ")"
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &link.PublicShare{
|
||||
Id: &link.PublicShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
Owner: rInfo.GetOwner(),
|
||||
Creator: u.Id,
|
||||
ResourceId: rInfo.Id,
|
||||
Token: tkn,
|
||||
Permissions: g.Permissions,
|
||||
Ctime: createdAt,
|
||||
Mtime: createdAt,
|
||||
PasswordProtected: passwordProtected,
|
||||
Expiration: g.Expiration,
|
||||
DisplayName: displayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// owncloud 10 prefixes the hash with `1|`
|
||||
func hashPassword(password string, cost int) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
return "1|" + string(bytes), err
|
||||
}
|
||||
|
||||
// UpdatePublicShare updates the expiration date, permissions and Mtime
|
||||
func (m *mgr) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
|
||||
query := "update oc_share set "
|
||||
paramsMap := map[string]interface{}{}
|
||||
params := []interface{}{}
|
||||
|
||||
now := time.Now().Unix()
|
||||
uid := u.Username
|
||||
|
||||
switch req.GetUpdate().GetType() {
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
|
||||
paramsMap["share_name"] = req.Update.GetDisplayName()
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
|
||||
paramsMap["permissions"] = sharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
|
||||
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
|
||||
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
|
||||
if req.Update.GetGrant().Password == "" {
|
||||
paramsMap["share_with"] = ""
|
||||
} else {
|
||||
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not hash share password")
|
||||
}
|
||||
paramsMap["share_with"] = h
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
|
||||
}
|
||||
|
||||
for k, v := range paramsMap {
|
||||
query += k + "=?"
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Ref.GetId() != nil:
|
||||
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
|
||||
case req.Ref.GetToken() != "":
|
||||
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, now, req.Ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(req.Ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetPublicShare(ctx, u, req.Ref, false)
|
||||
}
|
||||
|
||||
func (m *mgr) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
|
||||
|
||||
ps, err := m.getWithPassword(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicshare.IsExpired(&ps.PublicShare) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound("public share has expired")
|
||||
}
|
||||
|
||||
if ps.PublicShare.PasswordProtected && sign {
|
||||
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &ps.PublicShare, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
|
||||
switch {
|
||||
case ref.GetToken() != "":
|
||||
return m.getByToken(ctx, ref.GetToken())
|
||||
case ref.GetId().GetOpaqueId() != "":
|
||||
return m.getByID(ctx, ref.GetId().GetOpaqueId())
|
||||
default:
|
||||
return nil, errtypes.BadRequest("neither id nor token given")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mgr) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
|
||||
s, err := getByToken(m.db, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps, err := m.ConvertToCS3PublicShare(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &publicshare.WithPassword{
|
||||
Password: strings.TrimPrefix(s.ShareWith, "1|"),
|
||||
}
|
||||
proto.Merge(&ret.PublicShare, ps)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func getByToken(db *sql.DB, token string) (DBShare, error) {
|
||||
s := DBShare{Token: token}
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.id, s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE share_type=? AND token=?`
|
||||
if err := db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return s, errtypes.NotFound(token)
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
|
||||
s := DBShare{ID: id}
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE share_type=? AND id=?`
|
||||
if err := m.db.QueryRow(query, publicShareType, id).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ps, err := m.ConvertToCS3PublicShare(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := &publicshare.WithPassword{
|
||||
Password: strings.TrimPrefix(s.ShareWith, "1|"),
|
||||
}
|
||||
proto.Merge(&ret.PublicShare, ps)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
|
||||
uid := u.Username
|
||||
// FIXME instead of joining we may want to have to do a stat call ... if we want to store shares from other providers? or just Dump()? and be done with migration?
|
||||
query := `SELECT
|
||||
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
|
||||
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
|
||||
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
|
||||
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
|
||||
s.id, s.stime, s.permissions, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE (uid_owner=? or uid_initiator=?)
|
||||
AND (share_type=?)`
|
||||
var resourceFilters, ownerFilters, creatorFilters, storageFilters string
|
||||
var resourceParams, ownerParams, creatorParams, storageParams []interface{}
|
||||
params := []interface{}{uid, uid, publicShareType}
|
||||
|
||||
for _, f := range filters {
|
||||
switch f.Type {
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
|
||||
if len(resourceFilters) != 0 {
|
||||
resourceFilters += " OR "
|
||||
}
|
||||
resourceFilters += "item_source=?"
|
||||
resourceParams = append(resourceParams, f.GetResourceId().GetOpaqueId())
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
|
||||
if len(ownerFilters) != 0 {
|
||||
ownerFilters += " OR "
|
||||
}
|
||||
ownerFilters += "(uid_owner=?)"
|
||||
ownerParams = append(ownerParams, formatUserID(f.GetOwner()))
|
||||
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
|
||||
if len(creatorFilters) != 0 {
|
||||
creatorFilters += " OR "
|
||||
}
|
||||
creatorFilters += "(uid_initiator=?)"
|
||||
creatorParams = append(creatorParams, formatUserID(f.GetCreator()))
|
||||
case publicshare.StorageIDFilterType:
|
||||
if len(storageFilters) != 0 {
|
||||
storageFilters += " OR "
|
||||
}
|
||||
storageFilters += "(storage=?)"
|
||||
storageParams = append(storageParams, f.GetResourceId().GetStorageId())
|
||||
}
|
||||
}
|
||||
if resourceFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
|
||||
params = append(params, resourceParams...)
|
||||
}
|
||||
if ownerFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
|
||||
params = append(params, ownerParams...)
|
||||
}
|
||||
if creatorFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
|
||||
params = append(params, creatorParams...)
|
||||
}
|
||||
if storageFilters != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, storageFilters)
|
||||
params = append(params, storageParams...)
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*link.PublicShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
|
||||
continue
|
||||
}
|
||||
var cs3Share *link.PublicShare
|
||||
if cs3Share, err = m.ConvertToCS3PublicShare(ctx, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if publicshare.IsExpired(cs3Share) {
|
||||
_ = m.cleanupExpiredShares()
|
||||
} else {
|
||||
if cs3Share.PasswordProtected && sign {
|
||||
if err := publicshare.AddSignature(cs3Share, strings.TrimPrefix(s.ShareWith, "1|")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shares = append(shares, cs3Share)
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
|
||||
uid := u.Username
|
||||
query := "delete from oc_share where "
|
||||
params := []interface{}{}
|
||||
|
||||
switch {
|
||||
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
|
||||
query += "id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetToken() != "":
|
||||
query += "token=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetToken(), uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.Exec(params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
|
||||
ps, err := m.getByToken(ctx, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if publicshare.IsExpired(&ps.PublicShare) {
|
||||
if err := m.cleanupExpiredShares(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errtypes.NotFound("public share has expired")
|
||||
}
|
||||
|
||||
if ps.PublicShare.PasswordProtected {
|
||||
if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
|
||||
return nil, errtypes.InvalidCredentials("access denied")
|
||||
}
|
||||
}
|
||||
|
||||
return &ps.PublicShare, nil
|
||||
}
|
||||
|
||||
func (m *mgr) cleanupExpiredShares() error {
|
||||
if !m.c.EnableExpiredSharesCleanup {
|
||||
return nil
|
||||
}
|
||||
|
||||
query := "DELETE FROM oc_share WHERE expiration IS NOT NULL AND expiration < ?"
|
||||
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = stmt.Exec(params...); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
-155
@@ -1,155 +0,0 @@
|
||||
// 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 cbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"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/rgrpc/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/cache"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/token/manager/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("cbox", New)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
EOSNamespace string `mapstructure:"namespace"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
JWTSecret string `mapstructure:"jwt_secret"`
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
conf *config
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
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 an implementation of cache warmup that connects to the cbox share db and stats resources on EOS
|
||||
func New(m map[string]interface{}) (cache.Warmup, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &manager{
|
||||
conf: c,
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *manager) GetResourceInfos() ([]*provider.ResourceInfo, error) {
|
||||
query := "select coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source FROM oc_share WHERE (orphan = 0 or orphan IS NULL)"
|
||||
rows, err := m.db.Query(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tokenManager, err := jwt.New(map[string]interface{}{
|
||||
"secret": m.conf.JWTSecret,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := &userpb.User{
|
||||
Id: &userpb.UserId{
|
||||
OpaqueId: "root",
|
||||
},
|
||||
UidNumber: 0,
|
||||
GidNumber: 0,
|
||||
}
|
||||
scope, err := scope.AddOwnerScope(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenManager.MintToken(context.Background(), u, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), ctxpkg.TokenHeader, tkn)
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(m.conf.GatewaySvc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
infos := []*provider.ResourceInfo{}
|
||||
for rows.Next() {
|
||||
var spaceID, nodeID string
|
||||
if err := rows.Scan(&spaceID, &nodeID); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
statReq := provider.StatRequest{Ref: &provider.Reference{
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: spaceID,
|
||||
OpaqueId: nodeID,
|
||||
},
|
||||
}}
|
||||
|
||||
statRes, err := client.Stat(ctx, &statReq)
|
||||
if err != nil || statRes.Status.Code != rpc.Code_CODE_OK {
|
||||
continue
|
||||
}
|
||||
|
||||
infos = append(infos, statRes.Info)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return infos, nil
|
||||
|
||||
}
|
||||
+2
-5
@@ -18,8 +18,5 @@
|
||||
|
||||
package loader
|
||||
|
||||
import (
|
||||
// Load share cache drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/cache/warmup/cbox"
|
||||
// Add your own here
|
||||
)
|
||||
// Load share cache drivers.
|
||||
// Add your own here
|
||||
|
||||
-617
@@ -1,617 +0,0 @@
|
||||
// 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 json
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
rpcv1beta1 "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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/golang/protobuf/proto" // nolint:staticcheck // we need the legacy package to convert V1 to V2 messages
|
||||
"github.com/google/uuid"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"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/todo/pool"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
"google.golang.org/protobuf/encoding/prototext"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("json", New)
|
||||
}
|
||||
|
||||
// New returns a new mgr.
|
||||
func New(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.GatewayAddr == "" {
|
||||
return nil, errors.New("share manager config is missing gateway address")
|
||||
}
|
||||
|
||||
c.init()
|
||||
|
||||
// load or create file
|
||||
model, err := loadOrCreate(c.File)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error loading the file containing the shares")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgr{
|
||||
c: c,
|
||||
model: model,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadOrCreate(file string) (*shareModel, error) {
|
||||
if info, err := os.Stat(file); errors.Is(err, fs.ErrNotExist) || info.Size() == 0 {
|
||||
if err := os.WriteFile(file, []byte("{}"), 0700); err != nil {
|
||||
err = errors.Wrap(err, "error opening/creating the file: "+file)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile(file, os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error opening/creating the file: "+file)
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
data, err := io.ReadAll(fd)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error reading the data")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
j := &jsonEncoding{}
|
||||
if err := json.Unmarshal(data, j); err != nil {
|
||||
err = errors.Wrap(err, "error decoding data from json")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &shareModel{State: j.State, MountPoint: j.MountPoint}
|
||||
for _, s := range j.Shares {
|
||||
var decShare collaboration.Share
|
||||
if err = utils.UnmarshalJSONToProtoV1([]byte(s), &decShare); err != nil {
|
||||
return nil, errors.Wrap(err, "error decoding share from json")
|
||||
}
|
||||
m.Shares = append(m.Shares, &decShare)
|
||||
}
|
||||
|
||||
if m.State == nil {
|
||||
m.State = map[string]map[string]collaboration.ShareState{}
|
||||
}
|
||||
if m.MountPoint == nil {
|
||||
m.MountPoint = map[string]map[string]*provider.Reference{}
|
||||
}
|
||||
|
||||
m.file = file
|
||||
return m, nil
|
||||
}
|
||||
|
||||
type shareModel struct {
|
||||
file string
|
||||
State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState
|
||||
MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint
|
||||
Shares []*collaboration.Share `json:"shares"`
|
||||
}
|
||||
|
||||
type jsonEncoding struct {
|
||||
State map[string]map[string]collaboration.ShareState `json:"state"` // map[username]map[share_id]ShareState
|
||||
MountPoint map[string]map[string]*provider.Reference `json:"mount_point"` // map[username]map[share_id]MountPoint
|
||||
Shares []string `json:"shares"`
|
||||
}
|
||||
|
||||
func (m *shareModel) Save() error {
|
||||
j := &jsonEncoding{State: m.State, MountPoint: m.MountPoint}
|
||||
for _, s := range m.Shares {
|
||||
encShare, err := utils.MarshalProtoV1ToJSON(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error encoding to json")
|
||||
}
|
||||
j.Shares = append(j.Shares, string(encShare))
|
||||
}
|
||||
|
||||
data, err := json.Marshal(j)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error encoding to json")
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(m.file, data, 0644); err != nil {
|
||||
err = errors.Wrap(err, "error writing to file: "+m.file)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
c *config
|
||||
sync.Mutex // concurrent access to the file
|
||||
model *shareModel
|
||||
}
|
||||
|
||||
type config struct {
|
||||
File string `mapstructure:"file"`
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
}
|
||||
|
||||
func (c *config) init() {
|
||||
if c.File == "" {
|
||||
c.File = "/var/tmp/reva/shares.json"
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfig(m map[string]interface{}) (*config, error) {
|
||||
c := &config{}
|
||||
if err := mapstructure.Decode(m, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Dump exports shares and received shares to channels (e.g. during migration)
|
||||
func (m *mgr) Dump(ctx context.Context, shareChan chan<- *collaboration.Share, receivedShareChan chan<- share.ReceivedShareWithUser) error {
|
||||
log := appctx.GetLogger(ctx)
|
||||
for _, s := range m.model.Shares {
|
||||
shareChan <- s
|
||||
}
|
||||
|
||||
for userIDString, states := range m.model.State {
|
||||
userMountPoints := m.model.MountPoint[userIDString]
|
||||
id := &userv1beta1.UserId{}
|
||||
mV2 := proto.MessageV2(id)
|
||||
if err := prototext.Unmarshal([]byte(userIDString), mV2); err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling the user id")
|
||||
continue
|
||||
}
|
||||
|
||||
for shareIDString, state := range states {
|
||||
sid := &collaboration.ShareId{}
|
||||
mV2 := proto.MessageV2(sid)
|
||||
if err := prototext.Unmarshal([]byte(shareIDString), mV2); err != nil {
|
||||
log.Error().Err(err).Msg("error unmarshalling the user id")
|
||||
continue
|
||||
}
|
||||
|
||||
var s *collaboration.Share
|
||||
for _, is := range m.model.Shares {
|
||||
if is.Id.OpaqueId == sid.OpaqueId {
|
||||
s = is
|
||||
break
|
||||
}
|
||||
}
|
||||
if s == nil {
|
||||
log.Warn().Str("share id", sid.OpaqueId).Msg("Share not found")
|
||||
continue
|
||||
}
|
||||
|
||||
var mp *provider.Reference
|
||||
if userMountPoints != nil {
|
||||
mp = userMountPoints[shareIDString]
|
||||
}
|
||||
|
||||
receivedShareChan <- share.ReceivedShareWithUser{
|
||||
UserID: id,
|
||||
ReceivedShare: &collaboration.ReceivedShare{
|
||||
Share: s,
|
||||
State: state,
|
||||
MountPoint: mp,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
id := uuid.NewString()
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
now := time.Now().UnixNano()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now / int64(time.Second)),
|
||||
Nanos: uint32(now % int64(time.Second)),
|
||||
}
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errtypes.BadRequest("json: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, _, err := m.getByKey(key)
|
||||
if err == nil {
|
||||
// share already exists
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
s := &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: id,
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}
|
||||
|
||||
m.model.Shares = append(m.model.Shares, s)
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// getByID must be called in a lock-controlled block.
|
||||
func (m *mgr) getByID(id *collaboration.ShareId) (int, *collaboration.Share, error) {
|
||||
for i, s := range m.model.Shares {
|
||||
if s.GetId().OpaqueId == id.OpaqueId {
|
||||
return i, s, nil
|
||||
}
|
||||
}
|
||||
return -1, nil, errtypes.NotFound(id.String())
|
||||
}
|
||||
|
||||
// getByKey must be called in a lock-controlled block.
|
||||
func (m *mgr) getByKey(key *collaboration.ShareKey) (int, *collaboration.Share, error) {
|
||||
for i, s := range m.model.Shares {
|
||||
if (utils.UserEqual(key.Owner, s.Owner) || utils.UserEqual(key.Owner, s.Creator)) &&
|
||||
utils.ResourceIDEqual(key.ResourceId, s.ResourceId) && utils.GranteeEqual(key.Grantee, s.Grantee) {
|
||||
return i, s, nil
|
||||
}
|
||||
}
|
||||
return -1, nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
|
||||
// get must be called in a lock-controlled block.
|
||||
func (m *mgr) get(ref *collaboration.ShareReference) (idx int, s *collaboration.Share, err error) {
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
idx, s, err = m.getByID(ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
idx, s, err = m.getByKey(ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// check if we are the owner or the grantee
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if share.IsCreatedByUser(s, user) || share.IsGrantedToUser(s, user) {
|
||||
return s, nil
|
||||
}
|
||||
// we return not found to not disclose information
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
idx, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !share.IsCreatedByUser(s, user) {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
last := len(m.model.Shares) - 1
|
||||
m.model.Shares[idx] = m.model.Shares[last]
|
||||
// explicitly nil the reference to prevent memory leaks
|
||||
// https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order
|
||||
m.model.Shares[last] = nil
|
||||
m.model.Shares = m.model.Shares[:last]
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
var (
|
||||
idx int
|
||||
toUpdate *collaboration.Share
|
||||
)
|
||||
|
||||
if ref != nil {
|
||||
var err error
|
||||
idx, toUpdate, err = m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if updated != nil {
|
||||
var err error
|
||||
idx, toUpdate, err = m.getByID(updated.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if fieldMask != nil {
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "permissions":
|
||||
m.model.Shares[idx].Permissions = updated.Permissions
|
||||
case "expiration":
|
||||
m.model.Shares[idx].Expiration = updated.Expiration
|
||||
case "hidden":
|
||||
continue
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if !share.IsCreatedByUser(toUpdate, user) {
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
if p != nil {
|
||||
m.model.Shares[idx].Permissions = p
|
||||
}
|
||||
m.model.Shares[idx].Mtime = &typespb.Timestamp{
|
||||
Seconds: uint64(now / int64(time.Second)),
|
||||
Nanos: uint32(now % int64(time.Second)),
|
||||
}
|
||||
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
return m.model.Shares[idx], nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
log := appctx.GetLogger(ctx)
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list shares")
|
||||
}
|
||||
cache := make(map[string]struct{})
|
||||
var ss []*collaboration.Share
|
||||
for _, s := range m.model.Shares {
|
||||
if share.MatchesFilters(s, filters) {
|
||||
// Only add the share if the share was created by the user or if
|
||||
// the user has ListGrants permissions on the shared resource.
|
||||
// The ListGrants check is necessary when a space member wants
|
||||
// to list shares in a space.
|
||||
// We are using a cache here so that we don't have to stat a
|
||||
// resource multiple times.
|
||||
key := strings.Join([]string{s.ResourceId.StorageId, s.ResourceId.OpaqueId}, "!")
|
||||
if _, hit := cache[key]; !hit && !share.IsCreatedByUser(s, user) {
|
||||
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: s.ResourceId}})
|
||||
if err != nil || sRes.Status.Code != rpcv1beta1.Code_CODE_OK {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Interface("status", sRes.Status).
|
||||
Interface("resource_id", s.ResourceId).
|
||||
Msg("ListShares: could not stat resource")
|
||||
continue
|
||||
}
|
||||
if !sRes.Info.PermissionSet.ListGrants {
|
||||
continue
|
||||
}
|
||||
cache[key] = struct{}{}
|
||||
}
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userv1beta1.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
|
||||
gwc, err := pool.GetGatewayServiceClient(m.c.GatewayAddr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to list shares")
|
||||
}
|
||||
u, err := utils.GetUser(ctx, forUser, gwc)
|
||||
if err != nil {
|
||||
return nil, errtypes.BadRequest("user not found")
|
||||
}
|
||||
user = u
|
||||
}
|
||||
mem := make(map[string]int)
|
||||
var rss []*collaboration.ReceivedShare
|
||||
for _, s := range m.model.Shares {
|
||||
if !share.IsCreatedByUser(s, user) &&
|
||||
share.IsGrantedToUser(s, user) &&
|
||||
share.MatchesFilters(s, filters) {
|
||||
|
||||
rs := m.convert(user.Id, s)
|
||||
idx, seen := mem[s.ResourceId.OpaqueId]
|
||||
if !seen {
|
||||
rss = append(rss, rs)
|
||||
mem[s.ResourceId.OpaqueId] = len(rss) - 1
|
||||
continue
|
||||
}
|
||||
|
||||
// When we arrive here there was already a share for this resource.
|
||||
// if there is a mix-up of shares of type group and shares of type user we need to deduplicate them, since it points
|
||||
// to the same resource. Leave the more explicit and hide the less explicit. In this case we hide the group shares
|
||||
// and return the user share to the user.
|
||||
other := rss[idx]
|
||||
if other.Share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP && s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER {
|
||||
if other.State == rs.State {
|
||||
rss[idx] = rs
|
||||
} else {
|
||||
rss = append(rss, rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rss, nil
|
||||
}
|
||||
|
||||
// convert must be called in a lock-controlled block.
|
||||
func (m *mgr) convert(currentUser *userv1beta1.UserId, s *collaboration.Share) *collaboration.ReceivedShare {
|
||||
rs := &collaboration.ReceivedShare{
|
||||
Share: s,
|
||||
State: collaboration.ShareState_SHARE_STATE_PENDING,
|
||||
}
|
||||
if v, ok := m.model.State[currentUser.String()]; ok {
|
||||
if state, ok := v[s.Id.String()]; ok {
|
||||
rs.State = state
|
||||
}
|
||||
}
|
||||
if v, ok := m.model.MountPoint[currentUser.String()]; ok {
|
||||
if mp, ok := v[s.Id.String()]; ok {
|
||||
rs.MountPoint = mp
|
||||
}
|
||||
}
|
||||
return rs
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
return m.getReceived(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceived(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
_, s, err := m.get(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() != userv1beta1.UserType_USER_TYPE_SERVICE && !share.IsGrantedToUser(s, user) {
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
return m.convert(user.Id, s), nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, forUser *userv1beta1.UserId) (*collaboration.ReceivedShare, error) {
|
||||
rs, err := m.getReceived(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = receivedShare.State
|
||||
case "mount_point":
|
||||
rs.MountPoint = receivedShare.MountPoint
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
u := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := u.GetId().String()
|
||||
if u.GetId().GetType() == userv1beta1.UserType_USER_TYPE_SERVICE {
|
||||
uid = forUser.String()
|
||||
}
|
||||
// Persist state
|
||||
if v, ok := m.model.State[uid]; ok {
|
||||
v[rs.Share.Id.String()] = rs.State
|
||||
m.model.State[uid] = v
|
||||
} else {
|
||||
a := map[string]collaboration.ShareState{
|
||||
rs.Share.Id.String(): rs.State,
|
||||
}
|
||||
m.model.State[uid] = a
|
||||
}
|
||||
|
||||
// Persist mount point
|
||||
if v, ok := m.model.MountPoint[uid]; ok {
|
||||
v[rs.Share.Id.String()] = rs.MountPoint
|
||||
m.model.MountPoint[uid] = v
|
||||
} else {
|
||||
a := map[string]*provider.Reference{
|
||||
rs.Share.Id.String(): rs.MountPoint,
|
||||
}
|
||||
m.model.MountPoint[uid] = a
|
||||
}
|
||||
|
||||
if err := m.model.Save(); err != nil {
|
||||
err = errors.Wrap(err, "error saving model")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
-2
@@ -20,9 +20,7 @@ package loader
|
||||
|
||||
import (
|
||||
// Load core share manager drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/json"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/jsoncs3"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/memory"
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/share/manager/owncloudsql"
|
||||
// Add your own here
|
||||
)
|
||||
|
||||
Generated
Vendored
-300
@@ -1,300 +0,0 @@
|
||||
// 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 owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
// DBShare stores information about user and public shares.
|
||||
type DBShare struct {
|
||||
ID string
|
||||
UIDOwner string
|
||||
UIDInitiator string
|
||||
ItemStorage string
|
||||
FileSource string
|
||||
ShareWith string
|
||||
Token string
|
||||
Expiration string
|
||||
Permissions int
|
||||
ShareType int
|
||||
ShareName string
|
||||
STime int
|
||||
FileTarget string
|
||||
RejectedBy string
|
||||
State int
|
||||
Parent int
|
||||
}
|
||||
|
||||
// UserConverter describes an interface for converting user ids to names and back
|
||||
type UserConverter interface {
|
||||
UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error)
|
||||
UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error)
|
||||
GetUser(ctx context.Context, userid *userpb.UserId) (*userpb.User, error)
|
||||
}
|
||||
|
||||
// GatewayUserConverter converts usernames and ids using the gateway
|
||||
type GatewayUserConverter struct {
|
||||
gwAddr string
|
||||
|
||||
IDCache *ttlcache.Cache
|
||||
NameCache *ttlcache.Cache
|
||||
}
|
||||
|
||||
// NewGatewayUserConverter returns a instance of GatewayUserConverter
|
||||
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
|
||||
IDCache := ttlcache.NewCache()
|
||||
_ = IDCache.SetTTL(30 * time.Second)
|
||||
IDCache.SkipTTLExtensionOnHit(true)
|
||||
NameCache := ttlcache.NewCache()
|
||||
_ = NameCache.SetTTL(30 * time.Second)
|
||||
NameCache.SkipTTLExtensionOnHit(true)
|
||||
|
||||
return &GatewayUserConverter{
|
||||
gwAddr: gwAddr,
|
||||
IDCache: IDCache,
|
||||
NameCache: NameCache,
|
||||
}
|
||||
}
|
||||
|
||||
// UserIDToUserName converts a user ID to an username
|
||||
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
|
||||
username, err := c.NameCache.Get(userid.String())
|
||||
if err == nil {
|
||||
return username.(string), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
|
||||
UserId: userid,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
|
||||
return getUserResponse.User.Username, nil
|
||||
}
|
||||
|
||||
// UserNameToUserID converts a username to an user ID
|
||||
func (c *GatewayUserConverter) UserNameToUserID(ctx context.Context, username string) (*userpb.UserId, error) {
|
||||
id, err := c.IDCache.Get(username)
|
||||
if err == nil {
|
||||
return id.(*userpb.UserId), nil
|
||||
}
|
||||
|
||||
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
|
||||
Claim: "username",
|
||||
Value: username,
|
||||
SkipFetchingUserGroups: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
|
||||
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
|
||||
}
|
||||
_ = c.IDCache.Set(username, getUserResponse.User.Id)
|
||||
return getUserResponse.User.Id, nil
|
||||
}
|
||||
|
||||
// GetUser gets the user
|
||||
func (c *GatewayUserConverter) GetUser(ctx context.Context, userid *userpb.UserId) (*userpb.User, error) {
|
||||
gwc, err := pool.GetGatewayServiceClient(c.gwAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return utils.GetUser(ctx, userid, gwc)
|
||||
}
|
||||
|
||||
func (m *mgr) formatGrantee(ctx context.Context, g *provider.Grantee) (int, string, error) {
|
||||
var granteeType int
|
||||
var formattedID string
|
||||
switch g.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
granteeType = 0
|
||||
var err error
|
||||
formattedID, err = m.userConverter.UserIDToUserName(ctx, g.GetUserId())
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
granteeType = 1
|
||||
formattedID = formatGroupID(g.GetGroupId())
|
||||
default:
|
||||
granteeType = -1
|
||||
}
|
||||
return granteeType, formattedID, nil
|
||||
}
|
||||
|
||||
func (m *mgr) extractGrantee(ctx context.Context, t int, g string) (*provider.Grantee, error) {
|
||||
var grantee provider.Grantee
|
||||
switch t {
|
||||
case 0:
|
||||
userid, err := m.userConverter.UserNameToUserID(ctx, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
|
||||
grantee.Id = &provider.Grantee_UserId{UserId: userid}
|
||||
case 1, 2:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
|
||||
grantee.Id = &provider.Grantee_GroupId{GroupId: extractGroupID(g)}
|
||||
default:
|
||||
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
|
||||
}
|
||||
return &grantee, nil
|
||||
}
|
||||
|
||||
func resourceTypeToItem(r provider.ResourceType) string {
|
||||
switch r {
|
||||
case provider.ResourceType_RESOURCE_TYPE_FILE:
|
||||
return "file"
|
||||
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
|
||||
return "folder"
|
||||
case provider.ResourceType_RESOURCE_TYPE_REFERENCE:
|
||||
return "reference"
|
||||
case provider.ResourceType_RESOURCE_TYPE_SYMLINK:
|
||||
return "symlink"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func sharePermToInt(p *provider.ResourcePermissions) int {
|
||||
return int(conversions.RoleFromResourcePermissions(p, false).OCSPermissions())
|
||||
}
|
||||
|
||||
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
|
||||
perms, err := conversions.NewPermissions(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
|
||||
}
|
||||
|
||||
func intToShareState(g int) collaboration.ShareState {
|
||||
switch g {
|
||||
case 0:
|
||||
return collaboration.ShareState_SHARE_STATE_ACCEPTED
|
||||
case 1:
|
||||
return collaboration.ShareState_SHARE_STATE_PENDING
|
||||
case 2:
|
||||
return collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
default:
|
||||
return collaboration.ShareState_SHARE_STATE_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
func formatUserID(u *userpb.UserId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
func formatGroupID(u *grouppb.GroupId) string {
|
||||
return u.OpaqueId
|
||||
}
|
||||
|
||||
func extractGroupID(u string) *grouppb.GroupId {
|
||||
return &grouppb.GroupId{OpaqueId: u}
|
||||
}
|
||||
|
||||
func (m *mgr) convertToCS3Share(ctx context.Context, s DBShare, storageMountID string) (*collaboration.Share, error) {
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(s.STime),
|
||||
}
|
||||
permissions, err := intTosharePerm(s.Permissions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grantee, err := m.extractGrantee(ctx, s.ShareType, s.ShareWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var creator *userpb.UserId
|
||||
if s.UIDOwner == s.UIDInitiator {
|
||||
creator = owner
|
||||
} else {
|
||||
creator, err = m.userConverter.UserNameToUserID(ctx, s.UIDOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: s.ID,
|
||||
},
|
||||
ResourceId: &provider.ResourceId{
|
||||
SpaceId: s.ItemStorage,
|
||||
OpaqueId: s.FileSource,
|
||||
},
|
||||
Permissions: &collaboration.SharePermissions{Permissions: permissions},
|
||||
Grantee: grantee,
|
||||
Owner: owner,
|
||||
Creator: creator,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) convertToCS3ReceivedShare(ctx context.Context, s DBShare, storageMountID string) (*collaboration.ReceivedShare, error) {
|
||||
share, err := m.convertToCS3Share(ctx, s, storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var state collaboration.ShareState
|
||||
if s.RejectedBy != "" {
|
||||
state = collaboration.ShareState_SHARE_STATE_REJECTED
|
||||
} else {
|
||||
state = intToShareState(s.State)
|
||||
}
|
||||
return &collaboration.ReceivedShare{
|
||||
Share: share,
|
||||
State: state,
|
||||
MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")},
|
||||
}, nil
|
||||
}
|
||||
Generated
Vendored
-674
@@ -1,674 +0,0 @@
|
||||
// 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 owncloudsql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"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/utils"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
|
||||
// Provides mysql drivers
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
shareTypeUser = 0
|
||||
shareTypeGroup = 1
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("owncloudsql", NewMysql)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
GatewayAddr string `mapstructure:"gateway_addr"`
|
||||
StorageMountID string `mapstructure:"storage_mount_id"`
|
||||
DbUsername string `mapstructure:"db_username"`
|
||||
DbPassword string `mapstructure:"db_password"`
|
||||
DbHost string `mapstructure:"db_host"`
|
||||
DbPort int `mapstructure:"db_port"`
|
||||
DbName string `mapstructure:"db_name"`
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
driver string
|
||||
db *sql.DB
|
||||
storageMountID string
|
||||
userConverter UserConverter
|
||||
}
|
||||
|
||||
// NewMysql returns a new share manager connection to a mysql database
|
||||
func NewMysql(m map[string]interface{}) (share.Manager, error) {
|
||||
c, err := parseConfig(m)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "error creating a new manager")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConverter := NewGatewayUserConverter(c.GatewayAddr)
|
||||
|
||||
return New("mysql", db, c.StorageMountID, userConverter)
|
||||
}
|
||||
|
||||
// New returns a new Cache instance connecting to the given sql.DB
|
||||
func New(driver string, db *sql.DB, storageMountID string, userConverter UserConverter) (share.Manager, error) {
|
||||
return &mgr{
|
||||
driver: driver,
|
||||
db: db,
|
||||
storageMountID: storageMountID,
|
||||
userConverter: userConverter,
|
||||
}, 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 (m *mgr) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
|
||||
// do not allow share to myself or the owner if share is for a user
|
||||
// TODO(labkode): should not this be caught already at the gw level?
|
||||
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
|
||||
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
|
||||
return nil, errtypes.BadRequest("owncloudsql: owner/creator and grantee are the same")
|
||||
}
|
||||
|
||||
// check if share already exists.
|
||||
key := &collaboration.ShareKey{
|
||||
Owner: md.Owner,
|
||||
ResourceId: md.Id,
|
||||
Grantee: g.Grantee,
|
||||
}
|
||||
_, err := m.getByKey(ctx, key)
|
||||
|
||||
// share already exists
|
||||
if err == nil {
|
||||
return nil, errtypes.AlreadyExists(key.String())
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ts := &typespb.Timestamp{
|
||||
Seconds: uint64(now),
|
||||
}
|
||||
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, md.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, g.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
itemType := resourceTypeToItem(md.Type)
|
||||
targetPath := path.Join("/", path.Base(md.Path))
|
||||
permissions := sharePermToInt(g.Permissions.Permissions)
|
||||
itemSource := md.Id.OpaqueId
|
||||
fileSource, err := strconv.ParseUint(itemSource, 10, 64)
|
||||
if err != nil {
|
||||
// it can be the case that the item source may be a character string
|
||||
// we leave fileSource blank in that case
|
||||
fileSource = 0
|
||||
}
|
||||
|
||||
stmtString := "INSERT INTO oc_share (share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,share_with,file_target) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
stmtValues := []interface{}{shareType, owner, user.Username, itemType, itemSource, fileSource, permissions, now, shareWith, targetPath}
|
||||
|
||||
stmt, err := m.db.Prepare(stmtString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := stmt.ExecContext(ctx, stmtValues...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lastID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &collaboration.Share{
|
||||
Id: &collaboration.ShareId{
|
||||
OpaqueId: strconv.FormatInt(lastID, 10),
|
||||
},
|
||||
ResourceId: md.Id,
|
||||
Permissions: g.Permissions,
|
||||
Grantee: g.Grantee,
|
||||
Owner: md.Owner,
|
||||
Creator: user.Id,
|
||||
Ctime: ts,
|
||||
Mtime: ts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
|
||||
var s *collaboration.Share
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "DELETE FROM oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owner := formatUserID(key.Owner)
|
||||
query = "DELETE FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.ExecContext(ctx, params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rowCnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowCnt == 0 {
|
||||
return errtypes.NotFound(ref.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
|
||||
permissions := sharePermToInt(p.Permissions)
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
|
||||
var query string
|
||||
params := []interface{}{}
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
|
||||
case ref.GetKey() != nil:
|
||||
key := ref.GetKey()
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner := formatUserID(key.Owner)
|
||||
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
|
||||
default:
|
||||
return nil, errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = stmt.ExecContext(ctx, params...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.GetShare(ctx, ref)
|
||||
}
|
||||
|
||||
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
query := `
|
||||
SELECT
|
||||
coalesce(s.uid_owner, '') as uid_owner, coalesce(s.uid_initiator, '') as uid_initiator,
|
||||
coalesce(s.share_with, '') as share_with, coalesce(s.file_source, '') as file_source,
|
||||
s.file_target, s.id, s.stime, s.permissions, s.share_type, fc.storage as storage
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
|
||||
WHERE (uid_owner=? or uid_initiator=?)
|
||||
`
|
||||
params := []interface{}{uid, uid}
|
||||
|
||||
var (
|
||||
filterQuery string
|
||||
filterParams []interface{}
|
||||
err error
|
||||
)
|
||||
if len(filters) == 0 {
|
||||
filterQuery += "(share_type=? OR share_type=?)"
|
||||
params = append(params, shareTypeUser)
|
||||
params = append(params, shareTypeGroup)
|
||||
} else {
|
||||
filterQuery, filterParams, err = translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
}
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*collaboration.Share{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.ItemStorage); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
// we list the shares that are targeted to the user in context or to the user groups.
|
||||
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, forUser *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if user.GetId().GetType() == userpb.UserType_USER_TYPE_SERVICE {
|
||||
u, err := m.userConverter.GetUser(ctx, forUser)
|
||||
if err != nil {
|
||||
return nil, errtypes.BadRequest("user not found")
|
||||
}
|
||||
user = u
|
||||
}
|
||||
uid := user.Username
|
||||
|
||||
params := []interface{}{uid, uid, uid}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
homeConcat := ""
|
||||
if m.driver == "mysql" { // mysql concat
|
||||
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
|
||||
} else { // sqlite3 concat
|
||||
homeConcat = "storages.id = 'home::' || s.uid_owner"
|
||||
}
|
||||
userSelect := ""
|
||||
if len(user.Groups) > 0 {
|
||||
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
userSelect = "AND (share_type != 1 AND share_with=?)"
|
||||
}
|
||||
query := `
|
||||
WITH results AS
|
||||
(
|
||||
SELECT s.*, storages.numeric_id FROM oc_share s
|
||||
LEFT JOIN oc_storages storages ON ` + homeConcat + `
|
||||
WHERE (uid_owner != ? AND uid_initiator != ?) ` + userSelect + `
|
||||
)
|
||||
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
|
||||
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
|
||||
r.numeric_id, COALESCE(r.parent, -1) AS parent FROM results r LEFT JOIN results r2 ON r.id = r2.parent WHERE r.parent IS NULL`
|
||||
|
||||
filterQuery, filterParams, err := translateFilters(filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params = append(params, filterParams...)
|
||||
|
||||
if filterQuery != "" {
|
||||
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
|
||||
}
|
||||
query += ";"
|
||||
rows, err := m.db.QueryContext(ctx, query, params...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var s DBShare
|
||||
shares := []*collaboration.ReceivedShare{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
|
||||
continue
|
||||
}
|
||||
share, err := m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, share)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
}
|
||||
|
||||
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
|
||||
var s *collaboration.ReceivedShare
|
||||
var err error
|
||||
switch {
|
||||
case ref.GetId() != nil:
|
||||
s, err = m.getReceivedByID(ctx, ref.GetId())
|
||||
case ref.GetKey() != nil:
|
||||
s, err = m.getReceivedByKey(ctx, ref.GetKey())
|
||||
default:
|
||||
err = errtypes.NotFound(ref.String())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *mgr) UpdateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
|
||||
// TODO: How to inject the uid when a UserId is set? override it in the ctx? Add parameter to GetReceivedShare?
|
||||
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: receivedShare.Share.Id}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields := []string{}
|
||||
params := []interface{}{}
|
||||
for i := range fieldMask.Paths {
|
||||
switch fieldMask.Paths[i] {
|
||||
case "state":
|
||||
rs.State = receivedShare.State
|
||||
fields = append(fields, "accepted=?")
|
||||
switch rs.State {
|
||||
case collaboration.ShareState_SHARE_STATE_REJECTED:
|
||||
params = append(params, 2)
|
||||
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
|
||||
params = append(params, 0)
|
||||
}
|
||||
case "mount_point":
|
||||
fields = append(fields, "file_target=?")
|
||||
rs.MountPoint = receivedShare.MountPoint
|
||||
params = append(params, rs.MountPoint.Path)
|
||||
case "hidden":
|
||||
continue
|
||||
default:
|
||||
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return nil, fmt.Errorf("no valid field provided in the fieldmask")
|
||||
}
|
||||
|
||||
updateReceivedShare := func(column string) error {
|
||||
query := "update oc_share set "
|
||||
query += strings.Join(fields, ",")
|
||||
query += fmt.Sprintf(" where %s=?", column)
|
||||
params := append(params, rs.Share.Id.OpaqueId)
|
||||
|
||||
stmt, err := m.db.Prepare(query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := stmt.ExecContext(ctx, params...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
affected, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if affected < 1 {
|
||||
return fmt.Errorf("no rows updated")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err = updateReceivedShare("parent") // Try to update the child state in case of group shares first
|
||||
if err != nil {
|
||||
err = updateReceivedShare("id")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
s := DBShare{ID: id.OpaqueId}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, stime, permissions, share_type FROM oc_share WHERE id=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err := m.db.QueryRowContext(ctx, query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
|
||||
owner, err := m.userConverter.UserIDToUserName(ctx, key.Owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uid := ctxpkg.ContextMustGetUser(ctx).Username
|
||||
|
||||
s := DBShare{}
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
|
||||
if err = m.db.QueryRowContext(ctx, query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3Share(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := user.Username
|
||||
|
||||
params := []interface{}{id.OpaqueId, id.OpaqueId, uid} //nolint:prealloc
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
homeConcat := ""
|
||||
if m.driver == "mysql" { // mysql concat
|
||||
homeConcat = "storages.id = CONCAT('home::', s.uid_owner)"
|
||||
} else { // sqlite3 concat
|
||||
homeConcat = "storages.id = 'home::' || s.uid_owner"
|
||||
}
|
||||
userSelect := ""
|
||||
if len(user.Groups) > 0 {
|
||||
userSelect = "AND ((share_type != 1 AND share_with=?) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
|
||||
} else {
|
||||
userSelect = "AND (share_type != 1 AND share_with=?)"
|
||||
}
|
||||
|
||||
query := `
|
||||
WITH results AS
|
||||
(
|
||||
SELECT s.*, storages.numeric_id
|
||||
FROM oc_share s
|
||||
LEFT JOIN oc_storages storages ON ` + homeConcat + `
|
||||
WHERE s.id=? OR s.parent=? ` + userSelect + `
|
||||
)
|
||||
SELECT COALESCE(r.uid_owner, '') AS uid_owner, COALESCE(r.uid_initiator, '') AS uid_initiator, COALESCE(r.share_with, '')
|
||||
AS share_with, COALESCE(r.file_source, '') AS file_source, COALESCE(r2.file_target, r.file_target), r.id, r.stime, r.permissions, r.share_type, COALESCE(r2.accepted, r.accepted),
|
||||
r.numeric_id, COALESCE(r.parent, -1) AS parent
|
||||
FROM results r
|
||||
LEFT JOIN results r2 ON r.id = r2.parent
|
||||
WHERE r.parent IS NULL;
|
||||
`
|
||||
|
||||
s := DBShare{}
|
||||
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage, &s.Parent); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(id.OpaqueId)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
uid := user.Username
|
||||
|
||||
shareType, shareWith, err := m.formatGrantee(ctx, key.Grantee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := []interface{}{uid, formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith}
|
||||
for _, v := range user.Groups {
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
s := DBShare{}
|
||||
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source, file_target, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND file_source=? AND share_type=? AND share_with=? "
|
||||
if len(user.Groups) > 0 {
|
||||
query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))"
|
||||
} else {
|
||||
query += "AND (share_with=?)"
|
||||
}
|
||||
|
||||
if err := m.db.QueryRowContext(ctx, query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errtypes.NotFound(key.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return m.convertToCS3ReceivedShare(ctx, s, m.storageMountID)
|
||||
}
|
||||
|
||||
func granteeTypeToShareType(granteeType provider.GranteeType) int {
|
||||
switch granteeType {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
return shareTypeUser
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
return shareTypeGroup
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// translateFilters translates the filters to sql queries
|
||||
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
|
||||
var (
|
||||
filterQuery string
|
||||
params []interface{}
|
||||
)
|
||||
|
||||
groupedFilters := share.GroupFiltersByType(filters)
|
||||
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
|
||||
// That is why the filters got grouped by type.
|
||||
// For every given filter type, iterate over the filters and if there are more than one combine them.
|
||||
// Combine the different filter types using `AND`
|
||||
var filterCounter = 0
|
||||
for filterType, filters := range groupedFilters {
|
||||
switch filterType {
|
||||
case collaboration.Filter_TYPE_RESOURCE_ID:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "file_source=?"
|
||||
params = append(params, f.GetResourceId().OpaqueId)
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_GRANTEE_TYPE:
|
||||
filterQuery += "("
|
||||
for i, f := range filters {
|
||||
filterQuery += "r.share_type=?"
|
||||
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
|
||||
|
||||
if i != len(filters)-1 {
|
||||
filterQuery += " OR "
|
||||
}
|
||||
}
|
||||
filterQuery += ")"
|
||||
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
|
||||
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
|
||||
filterQuery += "r.permissions > 0"
|
||||
default:
|
||||
return "", nil, fmt.Errorf("filter type is not supported")
|
||||
}
|
||||
if filterCounter != len(groupedFilters)-1 {
|
||||
filterQuery += " AND "
|
||||
}
|
||||
filterCounter++
|
||||
}
|
||||
return filterQuery, params, nil
|
||||
}
|
||||
BIN
Binary file not shown.
-36
@@ -1,36 +0,0 @@
|
||||
// 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 favorite
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
)
|
||||
|
||||
// Manager defines an interface for a favorites manager.
|
||||
type Manager interface {
|
||||
// ListFavorites returns all resources that were favorited by a user.
|
||||
ListFavorites(ctx context.Context, userID *user.UserId) ([]*provider.ResourceId, error)
|
||||
// SetFavorite marks a resource as favorited by a user.
|
||||
SetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error
|
||||
// UnsetFavorite unmarks a resource as favorited by a user.
|
||||
UnsetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
// 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 share cache drivers.
|
||||
_ "github.com/opencloud-eu/reva/v2/pkg/storage/favorite/memory"
|
||||
// Add your own here
|
||||
)
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
// 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 memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registry.Register("memory", New)
|
||||
}
|
||||
|
||||
type mgr struct {
|
||||
sync.RWMutex
|
||||
favorites map[string]map[string]*provider.ResourceId
|
||||
}
|
||||
|
||||
// New returns an instance of the in-memory favorites manager.
|
||||
func New(m map[string]interface{}) (favorite.Manager, error) {
|
||||
return &mgr{favorites: make(map[string]map[string]*provider.ResourceId)}, nil
|
||||
}
|
||||
|
||||
func (m *mgr) ListFavorites(_ context.Context, userID *user.UserId) ([]*provider.ResourceId, error) {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
favorites := make([]*provider.ResourceId, 0, len(m.favorites[userID.OpaqueId]))
|
||||
for _, id := range m.favorites[userID.OpaqueId] {
|
||||
favorites = append(favorites, id)
|
||||
}
|
||||
return favorites, nil
|
||||
}
|
||||
|
||||
func (m *mgr) SetFavorite(_ context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.favorites[userID.OpaqueId] == nil {
|
||||
m.favorites[userID.OpaqueId] = make(map[string]*provider.ResourceId)
|
||||
}
|
||||
m.favorites[userID.OpaqueId][resourceInfo.Id.OpaqueId] = resourceInfo.Id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mgr) UnsetFavorite(_ context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
delete(m.favorites[userID.OpaqueId], resourceInfo.Id.OpaqueId)
|
||||
return nil
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// 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 registry
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
|
||||
|
||||
// NewFunc is the function that favorite storage implementations
|
||||
// should register at init time.
|
||||
type NewFunc func(map[string]interface{}) (favorite.Manager, error)
|
||||
|
||||
// NewFuncs is a map containing all the registered favorite storage implementations.
|
||||
var NewFuncs = map[string]NewFunc{}
|
||||
|
||||
// Register registers a new favorite storage function.
|
||||
// Not safe for concurrent use. Safe for use from package init.
|
||||
func Register(name string, f NewFunc) {
|
||||
NewFuncs[name] = f
|
||||
}
|
||||
+9
@@ -32,6 +32,7 @@ import (
|
||||
"strings"
|
||||
|
||||
cephfs2 "github.com/ceph/go-ceph/cephfs"
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/pkg/errors"
|
||||
@@ -602,6 +603,14 @@ func (fs *cephfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe
|
||||
return getRevaError(err)
|
||||
}
|
||||
|
||||
func (fs *cephfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *cephfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *cephfs) EmptyRecycle(ctx context.Context, ref *provider.Reference) error {
|
||||
return errtypes.NotSupported("cephfs: empty recycle not supported")
|
||||
}
|
||||
|
||||
+9
@@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
@@ -124,6 +125,14 @@ func (fs *hellofs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Ref
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
func (fs *hellofs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *hellofs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// locks
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
|
||||
+10
@@ -862,6 +862,16 @@ func (nc *StorageDriver) Unlock(ctx context.Context, ref *provider.Reference, lo
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
// AddFavorite adds a favourite to a resource
|
||||
func (nc *StorageDriver) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
// RemoveFavorite removes a favourite from a resource
|
||||
func (nc *StorageDriver) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// ListStorageSpaces as defined in the storage.FS interface
|
||||
func (nc *StorageDriver) ListStorageSpaces(ctx context.Context, f []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
|
||||
bodyStr, _ := json.Marshal(f)
|
||||
|
||||
+8
@@ -1137,6 +1137,14 @@ func (fs *owncloudsqlfs) UnsetArbitraryMetadata(ctx context.Context, ref *provid
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *owncloudsqlfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *owncloudsqlfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
func (fs *owncloudsqlfs) GetLock(ctx context.Context, ref *provider.Reference) (*provider.Lock, error) {
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
|
||||
+9
@@ -38,6 +38,7 @@ import (
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/appctx"
|
||||
@@ -806,3 +807,11 @@ func isLockFile(path string) bool {
|
||||
func isTrash(path string) bool {
|
||||
return strings.HasSuffix(path, ".trashinfo") || strings.HasSuffix(path, ".trashitem") || strings.Contains(path, ".Trash")
|
||||
}
|
||||
|
||||
func (t *Tree) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (t *Tree) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
Generated
Vendored
+46
@@ -1035,6 +1035,52 @@ func (fs *Decomposedfs) ListFolder(ctx context.Context, ref *provider.Reference,
|
||||
return finfos, nil
|
||||
}
|
||||
|
||||
// AddFavorite adds a favorite
|
||||
func (fs *Decomposedfs) AddFavorite(ctx context.Context, ref *provider.Reference, uid *user.UserId) error {
|
||||
ctx, span := tracer.Start(ctx, "AddFavorite")
|
||||
defer span.End()
|
||||
n, err := fs.lu.NodeFromResource(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !n.Exists {
|
||||
return errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
}
|
||||
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rp.Stat {
|
||||
return errtypes.PermissionDenied("stat")
|
||||
}
|
||||
|
||||
return n.SetFavorite(ctx, uid)
|
||||
}
|
||||
|
||||
// RemoveFavorite removes a favorite
|
||||
func (fs *Decomposedfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, uid *user.UserId) error {
|
||||
ctx, span := tracer.Start(ctx, "RemoveFavorite")
|
||||
defer span.End()
|
||||
n, err := fs.lu.NodeFromResource(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !n.Exists {
|
||||
return errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
}
|
||||
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !rp.Stat {
|
||||
return errtypes.PermissionDenied("stat")
|
||||
}
|
||||
|
||||
return n.UnsetFavorite(ctx, uid)
|
||||
}
|
||||
|
||||
// Delete deletes the specified resource
|
||||
func (fs *Decomposedfs) Delete(ctx context.Context, ref *provider.Reference) (err error) {
|
||||
ctx, span := tracer.Start(ctx, "Delete")
|
||||
|
||||
+7
-65
@@ -20,19 +20,14 @@ package decomposedfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "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/storage/pkg/decomposedfs/metadata"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/metadata/prefixes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storage/pkg/decomposedfs/node"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -44,8 +39,6 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider.
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Decomposedfs: error resolving ref")
|
||||
}
|
||||
sublog := appctx.GetLogger(ctx).With().Str("spaceid", n.SpaceID).Str("nodeid", n.ID).Logger()
|
||||
|
||||
if !n.Exists {
|
||||
err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
|
||||
return err
|
||||
@@ -91,25 +84,6 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider.
|
||||
errs = append(errs, errors.Wrap(err, "could not set etag"))
|
||||
}
|
||||
}
|
||||
if val, ok := md.Metadata[node.FavoriteKey]; ok {
|
||||
delete(md.Metadata, node.FavoriteKey)
|
||||
if u, ok := ctxpkg.ContextGetUser(ctx); ok {
|
||||
if uid := u.GetId(); uid != nil {
|
||||
if err := n.SetFavorite(ctx, uid, val); err != nil {
|
||||
sublog.Error().Err(err).
|
||||
Interface("user", u).
|
||||
Msg("could not set favorite flag")
|
||||
errs = append(errs, errors.Wrap(err, "could not set favorite flag"))
|
||||
}
|
||||
} else {
|
||||
sublog.Error().Interface("user", u).Msg("user has no id")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
|
||||
}
|
||||
} else {
|
||||
sublog.Error().Interface("user", u).Msg("error getting user from ctx")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range md.Metadata {
|
||||
attrName := prefixes.MetadataPrefix + k
|
||||
@@ -168,46 +142,14 @@ func (fs *Decomposedfs) UnsetArbitraryMetadata(ctx context.Context, ref *provide
|
||||
|
||||
errs := []error{}
|
||||
for _, k := range keys {
|
||||
switch k {
|
||||
case node.FavoriteKey:
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
sublog.Error().
|
||||
Interface("user", u).
|
||||
Msg("error getting user from ctx")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
|
||||
continue
|
||||
}
|
||||
var uid *userpb.UserId
|
||||
if uid = u.GetId(); uid == nil || uid.OpaqueId == "" {
|
||||
sublog.Error().
|
||||
Interface("user", u).
|
||||
Msg("user has no id")
|
||||
errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
|
||||
continue
|
||||
}
|
||||
fa := fmt.Sprintf("%s:%s:%s@%s", prefixes.FavPrefix, utils.UserTypeToString(uid.GetType()), uid.GetOpaqueId(), uid.GetIdp())
|
||||
if err := n.RemoveXattr(ctx, fa, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Interface("user", u).
|
||||
Str("key", fa).
|
||||
Msg("could not unset favorite flag")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset favorite flag"))
|
||||
}
|
||||
default:
|
||||
if err = n.RemoveXattr(ctx, prefixes.MetadataPrefix+k, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Str("key", k).
|
||||
Msg("could not unset metadata")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset metadata"))
|
||||
if err = n.RemoveXattr(ctx, prefixes.MetadataPrefix+k, true); err != nil {
|
||||
if metadata.IsAttrUnset(err) {
|
||||
continue // already gone, ignore
|
||||
}
|
||||
sublog.Error().Err(err).
|
||||
Str("key", k).
|
||||
Msg("could not unset metadata")
|
||||
errs = append(errs, errors.Wrap(err, "could not unset metadata"))
|
||||
}
|
||||
}
|
||||
switch len(errs) {
|
||||
|
||||
Generated
Vendored
+9
@@ -18,6 +18,10 @@
|
||||
|
||||
package prefixes
|
||||
|
||||
import (
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
)
|
||||
|
||||
// Declare a list of xattr keys
|
||||
|
||||
// Currently,extended file attributes have four separated
|
||||
@@ -102,3 +106,8 @@ const (
|
||||
UserAcePrefix string = "u:"
|
||||
GroupAcePrefix string = "g:"
|
||||
)
|
||||
|
||||
func FavoriteKey(uid *userpb.UserId) string {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
return FavPrefix + uid.OpaqueId
|
||||
}
|
||||
|
||||
+49
-25
@@ -723,26 +723,17 @@ func (n *Node) SetEtag(ctx context.Context, val string) (err error) {
|
||||
}
|
||||
|
||||
// SetFavorite sets the favorite for the current user
|
||||
// TODO we should not mess with the user here ... the favorites is now a user specific property for a file
|
||||
// that cannot be mapped to extended attributes without leaking who has marked a file as a favorite
|
||||
// it is a specific case of a tag, which is user individual as well
|
||||
// TODO there are different types of tags
|
||||
// 1. public that are managed by everyone
|
||||
// 2. private tags that are only visible to the user
|
||||
// 3. system tags that are only visible to the system
|
||||
// 4. group tags that are only visible to a group ...
|
||||
// urgh ... well this can be solved using different namespaces
|
||||
// 1. public = p:
|
||||
// 2. private = u:<uid>: for user specific
|
||||
// 3. system = s: for system
|
||||
// 4. group = g:<gid>:
|
||||
// 5. app? = a:<aid>: for apps?
|
||||
// obviously this only is secure when the u/s/g/a namespaces are not accessible by users in the filesystem
|
||||
// public tags can be mapped to extended attributes
|
||||
func (n *Node) SetFavorite(ctx context.Context, uid *userpb.UserId, val string) error {
|
||||
func (n *Node) SetFavorite(ctx context.Context, uid *userpb.UserId) error {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
fa := fmt.Sprintf("%s:%s:%s@%s", prefixes.FavPrefix, utils.UserTypeToString(uid.GetType()), uid.GetOpaqueId(), uid.GetIdp())
|
||||
return n.SetXattrString(ctx, fa, val)
|
||||
fa := prefixes.FavoriteKey(uid)
|
||||
return n.SetXattrString(ctx, fa, "1")
|
||||
}
|
||||
|
||||
// UnsetFavorite unsets the favorite for the current user
|
||||
func (n *Node) UnsetFavorite(ctx context.Context, uid *userpb.UserId) error {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
fa := prefixes.FavoriteKey(uid)
|
||||
return n.RemoveXattr(ctx, fa, true)
|
||||
}
|
||||
|
||||
// IsDir returns true if the node is a directory
|
||||
@@ -853,16 +844,15 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi
|
||||
|
||||
// read favorite flag for the current user
|
||||
if _, ok := mdKeysMap[FavoriteKey]; returnAllMetadata || ok {
|
||||
favorite := ""
|
||||
if u, ok := ctxpkg.ContextGetUser(ctx); ok {
|
||||
// the favorite flag is specific to the user, so we need to incorporate the userid
|
||||
if uid := u.GetId(); uid != nil {
|
||||
fa := fmt.Sprintf("%s:%s:%s@%s", prefixes.FavPrefix, utils.UserTypeToString(uid.GetType()), uid.GetOpaqueId(), uid.GetIdp())
|
||||
if val, err := n.XattrString(ctx, fa); err == nil {
|
||||
fa := prefixes.FavoriteKey(uid)
|
||||
if val, err := n.XattrString(ctx, fa); err == nil && val == "1" {
|
||||
sublog.Debug().
|
||||
Str("favorite", fa).
|
||||
Msg("found favorite flag")
|
||||
favorite = val
|
||||
metadata[FavoriteKey] = val
|
||||
}
|
||||
} else {
|
||||
sublog.Error().Err(errtypes.UserRequired("userrequired")).Msg("user has no id")
|
||||
@@ -870,15 +860,20 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi
|
||||
} else {
|
||||
sublog.Error().Err(errtypes.UserRequired("userrequired")).Msg("error getting user from ctx")
|
||||
}
|
||||
metadata[FavoriteKey] = favorite
|
||||
}
|
||||
|
||||
// read favorites
|
||||
if err = readFavoritesIntoOpaque(ctx, n, ri); err != nil {
|
||||
sublog.Debug().Err(err).Msg("error reading favorites")
|
||||
}
|
||||
|
||||
// read locks
|
||||
// FIXME move to fieldmask
|
||||
if _, ok := mdKeysMap[LockdiscoveryKey]; returnAllMetadata || ok {
|
||||
if n.hasLocks(ctx) {
|
||||
err = readLocksIntoOpaque(ctx, n, ri)
|
||||
if err != nil {
|
||||
sublog.Debug().Err(errtypes.InternalError("lockfail"))
|
||||
sublog.Debug().Err(errtypes.InternalError("lockfail")).Msg("error reading locks")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1493,3 +1488,32 @@ func ReadChildNodeFromLink(ctx context.Context, path string) (string, error) {
|
||||
nodeID = strings.ReplaceAll(nodeID, "/", "")
|
||||
return nodeID, nil
|
||||
}
|
||||
|
||||
func readFavoritesIntoOpaque(ctx context.Context, n *Node, ri *provider.ResourceInfo) error {
|
||||
attrs, err := n.Xattrs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
favorites := []string{}
|
||||
for key, value := range attrs {
|
||||
if string(value) == "1" && strings.HasPrefix(key, prefixes.FavPrefix) {
|
||||
favorites = append(favorites, key[len(prefixes.FavPrefix):])
|
||||
}
|
||||
}
|
||||
|
||||
var b []byte
|
||||
if b, err = json.Marshal(favorites); err != nil {
|
||||
appctx.GetLogger(ctx).Error().Err(err).Msg("Decomposedfs: could not marshal favorites")
|
||||
}
|
||||
if ri.Opaque == nil {
|
||||
ri.Opaque = &types.Opaque{
|
||||
Map: map[string]*types.OpaqueEntry{},
|
||||
}
|
||||
}
|
||||
ri.Opaque.Map["favorites"] = &types.OpaqueEntry{
|
||||
Decoder: "json",
|
||||
Value: b,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+6
@@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
@@ -116,6 +117,11 @@ type FS interface {
|
||||
// UnsetArbitraryMetadata removes arbitraty metadata from a resource
|
||||
UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error
|
||||
|
||||
// AddFavorite adds a favorite to a resource
|
||||
AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error
|
||||
// RemoveFavorite removes a favorite from a resource
|
||||
RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error
|
||||
|
||||
// Locks
|
||||
|
||||
// GetLock returns an existing lock on the given reference
|
||||
|
||||
Generated
Vendored
+8
@@ -1233,6 +1233,14 @@ func (fs *Decomposedfs) Unlock(ctx context.Context, ref *provider.Reference, loc
|
||||
return node.Unlock(ctx, lock)
|
||||
}
|
||||
|
||||
func (fs *Decomposedfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *Decomposedfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *user.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
|
||||
return fs.trashbin.ListRecycle(ctx, ref, key, relativePath)
|
||||
}
|
||||
|
||||
+8
@@ -281,6 +281,14 @@ func (fs *eosfs) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *eosfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *eosfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
func getUser(ctx context.Context) (*userpb.User, error) {
|
||||
u, ok := ctxpkg.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
|
||||
+8
@@ -722,6 +722,14 @@ func (fs *localfs) GetLock(ctx context.Context, ref *provider.Reference) (*provi
|
||||
return nil, errtypes.NotSupported("unimplemented")
|
||||
}
|
||||
|
||||
func (fs *localfs) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("AddFavorite not implemented")
|
||||
}
|
||||
|
||||
func (fs *localfs) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
return errtypes.NotSupported("RemoveFavorite not implemented")
|
||||
}
|
||||
|
||||
// SetLock puts a lock on the given reference
|
||||
func (fs *localfs) SetLock(ctx context.Context, ref *provider.Reference, lock *provider.Lock) error {
|
||||
return errtypes.NotSupported("unimplemented")
|
||||
|
||||
Generated
Vendored
+55
@@ -23,6 +23,7 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
|
||||
tusd "github.com/tus/tusd/v2/pkg/handler"
|
||||
|
||||
@@ -1088,3 +1089,57 @@ func (f *FS) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorage
|
||||
|
||||
return res0
|
||||
}
|
||||
|
||||
func (f *FS) AddFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
var (
|
||||
err error
|
||||
unhook UnHook
|
||||
unhooks []UnHook
|
||||
)
|
||||
for _, hook := range f.hooks {
|
||||
ctx, unhook, err = hook("AddFavorite", ctx, ref.GetResourceId().GetSpaceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unhook != nil {
|
||||
unhooks = append(unhooks, unhook)
|
||||
}
|
||||
}
|
||||
|
||||
res0 := f.next.AddFavorite(ctx, ref, userID)
|
||||
|
||||
for _, unhook := range unhooks {
|
||||
if err := unhook(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return res0
|
||||
}
|
||||
|
||||
func (f *FS) RemoveFavorite(ctx context.Context, ref *provider.Reference, userID *userpb.UserId) error {
|
||||
var (
|
||||
err error
|
||||
unhook UnHook
|
||||
unhooks []UnHook
|
||||
)
|
||||
for _, hook := range f.hooks {
|
||||
ctx, unhook, err = hook("RemoveFavorite", ctx, ref.GetResourceId().GetSpaceId())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if unhook != nil {
|
||||
unhooks = append(unhooks, unhook)
|
||||
}
|
||||
}
|
||||
|
||||
res0 := f.next.RemoveFavorite(ctx, ref, userID)
|
||||
|
||||
for _, unhook := range unhooks {
|
||||
if err := unhook(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return res0
|
||||
}
|
||||
|
||||
+146
@@ -222,6 +222,79 @@ func (_c *GatewayAPIClient_AddAppProvider_Call) RunAndReturn(run func(context.Co
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddFavorite provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayAPIClient) AddFavorite(ctx context.Context, in *providerv1beta1.AddFavoriteRequest, opts ...grpc.CallOption) (*providerv1beta1.AddFavoriteResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddFavorite")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.AddFavoriteResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.AddFavoriteRequest, ...grpc.CallOption) (*providerv1beta1.AddFavoriteResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.AddFavoriteRequest, ...grpc.CallOption) *providerv1beta1.AddFavoriteResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.AddFavoriteResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.AddFavoriteRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GatewayAPIClient_AddFavorite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFavorite'
|
||||
type GatewayAPIClient_AddFavorite_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddFavorite is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.AddFavoriteRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *GatewayAPIClient_Expecter) AddFavorite(ctx interface{}, in interface{}, opts ...interface{}) *GatewayAPIClient_AddFavorite_Call {
|
||||
return &GatewayAPIClient_AddFavorite_Call{Call: _e.mock.On("AddFavorite",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_AddFavorite_Call) Run(run func(ctx context.Context, in *providerv1beta1.AddFavoriteRequest, opts ...grpc.CallOption)) *GatewayAPIClient_AddFavorite_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.AddFavoriteRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_AddFavorite_Call) Return(_a0 *providerv1beta1.AddFavoriteResponse, _a1 error) *GatewayAPIClient_AddFavorite_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_AddFavorite_Call) RunAndReturn(run func(context.Context, *providerv1beta1.AddFavoriteRequest, ...grpc.CallOption) (*providerv1beta1.AddFavoriteResponse, error)) *GatewayAPIClient_AddFavorite_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Authenticate provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayAPIClient) Authenticate(ctx context.Context, in *gatewayv1beta1.AuthenticateRequest, opts ...grpc.CallOption) (*gatewayv1beta1.AuthenticateResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
@@ -5989,6 +6062,79 @@ func (_c *GatewayAPIClient_RefreshLock_Call) RunAndReturn(run func(context.Conte
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveFavorite provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayAPIClient) RemoveFavorite(ctx context.Context, in *providerv1beta1.RemoveFavoriteRequest, opts ...grpc.CallOption) (*providerv1beta1.RemoveFavoriteResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
if len(opts) > 0 {
|
||||
tmpRet = _m.Called(ctx, in, opts)
|
||||
} else {
|
||||
tmpRet = _m.Called(ctx, in)
|
||||
}
|
||||
ret := tmpRet
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveFavorite")
|
||||
}
|
||||
|
||||
var r0 *providerv1beta1.RemoveFavoriteResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.RemoveFavoriteRequest, ...grpc.CallOption) (*providerv1beta1.RemoveFavoriteResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.RemoveFavoriteRequest, ...grpc.CallOption) *providerv1beta1.RemoveFavoriteResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*providerv1beta1.RemoveFavoriteResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.RemoveFavoriteRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GatewayAPIClient_RemoveFavorite_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveFavorite'
|
||||
type GatewayAPIClient_RemoveFavorite_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveFavorite is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - in *providerv1beta1.RemoveFavoriteRequest
|
||||
// - opts ...grpc.CallOption
|
||||
func (_e *GatewayAPIClient_Expecter) RemoveFavorite(ctx interface{}, in interface{}, opts ...interface{}) *GatewayAPIClient_RemoveFavorite_Call {
|
||||
return &GatewayAPIClient_RemoveFavorite_Call{Call: _e.mock.On("RemoveFavorite",
|
||||
append([]interface{}{ctx, in}, opts...)...)}
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_RemoveFavorite_Call) Run(run func(ctx context.Context, in *providerv1beta1.RemoveFavoriteRequest, opts ...grpc.CallOption)) *GatewayAPIClient_RemoveFavorite_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
variadicArgs := make([]grpc.CallOption, len(args)-2)
|
||||
for i, a := range args[2:] {
|
||||
if a != nil {
|
||||
variadicArgs[i] = a.(grpc.CallOption)
|
||||
}
|
||||
}
|
||||
run(args[0].(context.Context), args[1].(*providerv1beta1.RemoveFavoriteRequest), variadicArgs...)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_RemoveFavorite_Call) Return(_a0 *providerv1beta1.RemoveFavoriteResponse, _a1 error) *GatewayAPIClient_RemoveFavorite_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *GatewayAPIClient_RemoveFavorite_Call) RunAndReturn(run func(context.Context, *providerv1beta1.RemoveFavoriteRequest, ...grpc.CallOption) (*providerv1beta1.RemoveFavoriteResponse, error)) *GatewayAPIClient_RemoveFavorite_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveOCMShare provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayAPIClient) RemoveOCMShare(ctx context.Context, in *ocmv1beta1.RemoveOCMShareRequest, opts ...grpc.CallOption) (*ocmv1beta1.RemoveOCMShareResponse, error) {
|
||||
var tmpRet mock.Arguments
|
||||
|
||||
Reference in New Issue
Block a user