Bump reva

This commit is contained in:
André Duffeck
2026-03-13 09:38:57 +01:00
parent cd0831aa10
commit 44549379ca
86 changed files with 2741 additions and 12188 deletions
-2
View File
@@ -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"
@@ -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
@@ -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...)
}
@@ -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()
@@ -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 == "" {
@@ -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{
@@ -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"
}
@@ -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),
@@ -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,
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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()
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
}
@@ -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.
)
@@ -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
}
@@ -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")
}
@@ -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
)
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
View File
@@ -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
}
@@ -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
)
@@ -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
}
@@ -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
}
Binary file not shown.
@@ -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
}
@@ -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
)
@@ -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
}
@@ -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
}
@@ -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")
}
@@ -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
@@ -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)
@@ -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")
@@ -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")
}
@@ -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")
@@ -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) {
@@ -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
}
@@ -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
View File
@@ -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
@@ -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)
}
@@ -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 {
@@ -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")
@@ -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
}
@@ -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