From ae59522dbb8c19c9b26ba7bd5d46c8622bbf7b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 2 Aug 2021 12:14:37 +0200 Subject: [PATCH 1/3] Add a share manager that connects to oc10 databases --- pkg/share/manager/loader/loader.go | 1 + pkg/share/manager/sql/conversions.go | 261 ++++++++++ pkg/share/manager/sql/mocks/UserConverter.go | 60 +++ pkg/share/manager/sql/sql.go | 502 +++++++++++++++++++ pkg/share/manager/sql/sql_suite_test.go | 13 + pkg/share/manager/sql/sql_test.go | 272 ++++++++++ pkg/share/manager/sql/test.db | Bin 0 -> 655360 bytes 7 files changed, 1109 insertions(+) create mode 100644 pkg/share/manager/sql/conversions.go create mode 100644 pkg/share/manager/sql/mocks/UserConverter.go create mode 100644 pkg/share/manager/sql/sql.go create mode 100644 pkg/share/manager/sql/sql_suite_test.go create mode 100644 pkg/share/manager/sql/sql_test.go create mode 100644 pkg/share/manager/sql/test.db diff --git a/pkg/share/manager/loader/loader.go b/pkg/share/manager/loader/loader.go index 46f7a057c2..b3ebbcae29 100644 --- a/pkg/share/manager/loader/loader.go +++ b/pkg/share/manager/loader/loader.go @@ -22,5 +22,6 @@ import ( // Load core share manager drivers. _ "github.com/cs3org/reva/pkg/share/manager/json" _ "github.com/cs3org/reva/pkg/share/manager/memory" + _ "github.com/cs3org/reva/pkg/share/manager/sql" // Add your own here ) diff --git a/pkg/share/manager/sql/conversions.go b/pkg/share/manager/sql/conversions.go new file mode 100644 index 0000000000..7abf2813d7 --- /dev/null +++ b/pkg/share/manager/sql/conversions.go @@ -0,0 +1,261 @@ +// 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" + + grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + userprovider "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" + conversions "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" +) + +//go:generate mockery -name UserConverter + +// DBShare stores information about user and public shares. +type DBShare struct { + ID string + UIDOwner string + UIDInitiator string + ItemStorage string + ItemSource string + ShareWith string + Token string + Expiration string + Permissions int + ShareType int + ShareName string + STime int + FileTarget string + RejectedBy string + State 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 +} + +// NewGatewayUserConverter returns a instance of GatewayUserConverter +func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter { + return &GatewayUserConverter{ + gwAddr: gwAddr, + } +} + +// UserIDToUserName converts a user ID to an username +func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) { + gwConn, err := pool.GetGatewayServiceClient(c.gwAddr) + if err != nil { + return "", err + } + getUserResponse, err := gwConn.GetUser(ctx, &userprovider.GetUserRequest{ + UserId: userid, + }) + if err != nil { + return "", err + } + if getUserResponse.Status.Code != rpc.Code_CODE_OK { + return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway") + } + 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) { + gwConn, err := pool.GetGatewayServiceClient(c.gwAddr) + if err != nil { + return nil, err + } + getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{ + Claim: "username", + Value: username, + }) + if err != nil { + return nil, err + } + if getUserResponse.Status.Code != rpc.Code_CODE_OK { + return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway") + } + return getUserResponse.User.Id, nil +} + +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: + 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).OCSPermissions()) +} + +func intTosharePerm(p int) (*provider.ResourcePermissions, error) { + perms, err := conversions.NewPermissions(p) + if err != nil { + return nil, err + } + + return conversions.RoleFromOCSPermissions(perms).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{ + StorageId: storageMountID + "!" + s.ItemStorage, + OpaqueId: s.ItemSource, + }, + 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, + }, nil +} diff --git a/pkg/share/manager/sql/mocks/UserConverter.go b/pkg/share/manager/sql/mocks/UserConverter.go new file mode 100644 index 0000000000..470ae2999b --- /dev/null +++ b/pkg/share/manager/sql/mocks/UserConverter.go @@ -0,0 +1,60 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" +) + +// UserConverter is an autogenerated mock type for the UserConverter type +type UserConverter struct { + mock.Mock +} + +// UserIDToUserName provides a mock function with given fields: ctx, userid +func (_m *UserConverter) UserIDToUserName(ctx context.Context, userid *userv1beta1.UserId) (string, error) { + ret := _m.Called(ctx, userid) + + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.UserId) string); ok { + r0 = rf(ctx, userid) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.UserId) error); ok { + r1 = rf(ctx, userid) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UserNameToUserID provides a mock function with given fields: ctx, username +func (_m *UserConverter) UserNameToUserID(ctx context.Context, username string) (*userv1beta1.UserId, error) { + ret := _m.Called(ctx, username) + + var r0 *userv1beta1.UserId + if rf, ok := ret.Get(0).(func(context.Context, string) *userv1beta1.UserId); ok { + r0 = rf(ctx, username) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*userv1beta1.UserId) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, username) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/share/manager/sql/sql.go b/pkg/share/manager/sql/sql.go new file mode 100644 index 0000000000..18e534ad35 --- /dev/null +++ b/pkg/share/manager/sql/sql.go @@ -0,0 +1,502 @@ +// 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" + + 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/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/share" + "github.com/cs3org/reva/pkg/share/manager/registry" + "github.com/cs3org/reva/pkg/user" + "github.com/cs3org/reva/pkg/utils" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + // Provides mysql drivers + _ "github.com/go-sql-driver/mysql" +) + +func init() { + registry.Register("oc10-sql", 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 := user.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), + } + + 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.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) 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 := user.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 item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" + params = append(params, owner, key.ResourceId.StorageId, 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) (*collaboration.Share, error) { + permissions := sharePermToInt(p.Permissions) + uid := user.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 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.StorageId, 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.ListSharesRequest_Filter) ([]*collaboration.Share, error) { + uid := user.ContextMustGetUser(ctx).Username + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)" + var filterQuery string + params := []interface{}{uid, uid, 0, 1} + for i, f := range filters { + if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + filterQuery += "(item_source=?)" + if i != len(filters)-1 { + filterQuery += " AND " + } + params = append(params, f.GetResourceId().OpaqueId) + } else { + return nil, fmt.Errorf("filter type is not supported") + } + } + 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 DBShare + shares := []*collaboration.Share{} + for rows.Next() { + if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType); 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) ([]*collaboration.ReceivedShare, error) { + user := user.ContextMustGetUser(ctx) + uid := user.Username + + params := []interface{}{uid, uid, uid} + for _, v := range user.Groups { + params = append(params, v) + } + + homeConcat := "" + if m.driver == "mysql" { // mysql upsert + homeConcat = "storages.id = CONCAT('home::', ts.uid_owner)" + } else { // sqlite3 upsert + homeConcat = "storages.id = 'home::' || ts.uid_owner" + } + query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, ts.id, stime, permissions, share_type, accepted, storages.numeric_id FROM oc_share ts LEFT JOIN oc_storages storages ON " + homeConcat + " WHERE (uid_owner != ? AND uid_initiator != ?) " + if len(user.Groups) > 0 { + query += "AND (share_with=? OR share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + "))" + } else { + query += "AND (share_with=?)" + } + + rows, err := m.db.Query(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.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State, &s.ItemStorage); 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, ref *collaboration.ShareReference, f *collaboration.UpdateReceivedShareRequest_UpdateField) (*collaboration.ReceivedShare, error) { + rs, err := m.GetReceivedShare(ctx, ref) + if err != nil { + return nil, err + } + + var queryAccept string + switch f.GetState() { + case collaboration.ShareState_SHARE_STATE_REJECTED: + queryAccept = "update oc_share set accepted=2 where id=?" + case collaboration.ShareState_SHARE_STATE_ACCEPTED: + queryAccept = "update oc_share set accepted=0 where id=?" + } + + if queryAccept != "" { + stmt, err := m.db.Prepare(queryAccept) + if err != nil { + return nil, err + } + _, err = stmt.Exec(rs.Share.Id.OpaqueId) + if err != nil { + return nil, err + } + } + + rs.State = f.GetState() + return rs, nil +} + +func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) { + uid := user.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(item_source, '') as item_source, stime, permissions, share_type FROM oc_share WHERE 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.ItemSource, &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 := user.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(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE uid_owner=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" + if err = m.db.QueryRow(query, owner, key.ResourceId.StorageId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &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 := user.ContextMustGetUser(ctx) + uid := user.Username + + params := []interface{}{id.OpaqueId, uid} + for _, v := range user.Groups { + params = append(params, v) + } + + 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(item_source, '') as item_source, stime, permissions, share_type, accepted FROM oc_share ts WHERE ts.id=? " + 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.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &s.STime, &s.Permissions, &s.ShareType, &s.State); 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 := user.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(item_source, '') as item_source, ts.id, stime, permissions, share_type, accepted FROM oc_share ts WHERE uid_owner=? AND item_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.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.ItemSource, &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) +} diff --git a/pkg/share/manager/sql/sql_suite_test.go b/pkg/share/manager/sql/sql_suite_test.go new file mode 100644 index 0000000000..8d53a7c44a --- /dev/null +++ b/pkg/share/manager/sql/sql_suite_test.go @@ -0,0 +1,13 @@ +package sql_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSql(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Sql Suite") +} diff --git a/pkg/share/manager/sql/sql_test.go b/pkg/share/manager/sql/sql_test.go new file mode 100644 index 0000000000..bb4dffcdce --- /dev/null +++ b/pkg/share/manager/sql/sql_test.go @@ -0,0 +1,272 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql_test + +import ( + "context" + "database/sql" + "io/ioutil" + "os" + + user "github.com/cs3org/go-cs3apis/cs3/identity/user/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" + "github.com/cs3org/reva/pkg/share" + sqlmanager "github.com/cs3org/reva/pkg/share/manager/sql" + mocks "github.com/cs3org/reva/pkg/share/manager/sql/mocks" + ruser "github.com/cs3org/reva/pkg/user" + + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/mock" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SQL manager", func() { + var ( + mgr share.Manager + ctx context.Context + testDbFile *os.File + + loginAs = func(user *userpb.User) { + ctx = ruser.ContextSetUser(context.Background(), user) + } + admin = &userpb.User{ + Id: &userpb.UserId{ + Idp: "idp", + OpaqueId: "userid", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "admin", + } + otherUser = &userpb.User{ + Id: &userpb.UserId{ + Idp: "idp", + OpaqueId: "userid", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "einstein", + } + + shareRef = &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "1", + }, + }} + ) + + AfterEach(func() { + os.Remove(testDbFile.Name()) + }) + + BeforeEach(func() { + var err error + testDbFile, err = ioutil.TempFile("", "example") + Expect(err).ToNot(HaveOccurred()) + + dbData, err := ioutil.ReadFile("test.db") + Expect(err).ToNot(HaveOccurred()) + + _, err = testDbFile.Write(dbData) + Expect(err).ToNot(HaveOccurred()) + err = testDbFile.Close() + Expect(err).ToNot(HaveOccurred()) + + sqldb, err := sql.Open("sqlite3", testDbFile.Name()) + Expect(err).ToNot(HaveOccurred()) + + userConverter := &mocks.UserConverter{} + userConverter.On("UserIDToUserName", mock.Anything, mock.Anything).Return("username", nil) + userConverter.On("UserNameToUserID", mock.Anything, mock.Anything).Return( + func(_ context.Context, username string) *userpb.UserId { + return &userpb.UserId{ + OpaqueId: username, + } + }, + func(_ context.Context, username string) error { return nil }) + mgr, err = sqlmanager.New("sqlite3", sqldb, "abcde", userConverter) + Expect(err).ToNot(HaveOccurred()) + + loginAs(admin) + }) + + It("creates manager instances", func() { + Expect(mgr).ToNot(BeNil()) + }) + + Describe("GetShare", func() { + It("returns the share", func() { + share, err := mgr.GetShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + }) + + It("returns an error if the share does not exis", func() { + share, err := mgr.GetShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: "2", + }, + }}) + Expect(err).To(HaveOccurred()) + Expect(share).To(BeNil()) + }) + }) + + Describe("Share", func() { + It("creates a share", func() { + grant := &collaboration.ShareGrant{ + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{UserId: &user.UserId{ + OpaqueId: "someone", + }}, + }, + Permissions: &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + GetPath: true, + InitiateFileDownload: true, + ListFileVersions: true, + ListContainer: true, + Stat: true, + }, + }, + } + info := &provider.ResourceInfo{ + Id: &provider.ResourceId{ + StorageId: "/", + OpaqueId: "something", + }, + } + share, err := mgr.Share(ctx, info, grant) + + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.Id.OpaqueId).To(Equal("2")) + }) + }) + + Describe("ListShares", func() { + It("lists shares", func() { + shares, err := mgr.ListShares(ctx, []*collaboration.ListSharesRequest_Filter{}) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(1)) + + shares, err = mgr.ListShares(ctx, []*collaboration.ListSharesRequest_Filter{ + { + Type: collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID, + Term: &collaboration.ListSharesRequest_Filter_ResourceId{ + ResourceId: &provider.ResourceId{ + StorageId: "/", + OpaqueId: "somethingElse", + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(0)) + }) + }) + + Describe("ListReceivedShares", func() { + It("lists received shares", func() { + loginAs(otherUser) + shares, err := mgr.ListReceivedShares(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(1)) + }) + }) + + Describe("GetReceivedShare", func() { + It("returns the received share", func() { + loginAs(otherUser) + share, err := mgr.GetReceivedShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + }) + }) + + Describe("UpdateReceivedShare", func() { + It("updates the received share", func() { + loginAs(otherUser) + + share, err := mgr.GetReceivedShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_ACCEPTED)) + + share, err = mgr.UpdateReceivedShare(ctx, shareRef, &collaboration.UpdateReceivedShareRequest_UpdateField{ + Field: &collaboration.UpdateReceivedShareRequest_UpdateField_State{ + State: collaboration.ShareState_SHARE_STATE_REJECTED, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_REJECTED)) + + share, err = mgr.GetReceivedShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share).ToNot(BeNil()) + Expect(share.State).To(Equal(collaboration.ShareState_SHARE_STATE_REJECTED)) + }) + }) + + Describe("Unshare", func() { + It("deletes shares", func() { + loginAs(otherUser) + shares, err := mgr.ListReceivedShares(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(1)) + + loginAs(admin) + err = mgr.Unshare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{ + Id: &collaboration.ShareId{ + OpaqueId: shares[0].Share.Id.OpaqueId, + }, + }}) + Expect(err).ToNot(HaveOccurred()) + + loginAs(otherUser) + shares, err = mgr.ListReceivedShares(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(len(shares)).To(Equal(0)) + }) + }) + + Describe("UpdateShare", func() { + It("updates permissions", func() { + share, err := mgr.GetShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share.Permissions.Permissions.Delete).To(BeTrue()) + + share, err = mgr.UpdateShare(ctx, shareRef, &collaboration.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + }}) + Expect(err).ToNot(HaveOccurred()) + Expect(share.Permissions.Permissions.Delete).To(BeFalse()) + + share, err = mgr.GetShare(ctx, shareRef) + Expect(err).ToNot(HaveOccurred()) + Expect(share.Permissions.Permissions.Delete).To(BeFalse()) + }) + }) +}) diff --git a/pkg/share/manager/sql/test.db b/pkg/share/manager/sql/test.db new file mode 100644 index 0000000000000000000000000000000000000000..fba76fdcc4fc7816bf2b281f9c2074bf1955842c GIT binary patch literal 655360 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 literal 0 HcmV?d00001 From 9551dd7bb2764d58daf29ee2e837496c1211779c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 2 Aug 2021 12:33:55 +0200 Subject: [PATCH 2/3] Add changelog --- changelog/unreleased/add-oc10-sharemanager.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/unreleased/add-oc10-sharemanager.md diff --git a/changelog/unreleased/add-oc10-sharemanager.md b/changelog/unreleased/add-oc10-sharemanager.md new file mode 100644 index 0000000000..8a2c1646e8 --- /dev/null +++ b/changelog/unreleased/add-oc10-sharemanager.md @@ -0,0 +1,3 @@ +Enhancement: Add share manager that connects to oc10 databases + +https://github.com/cs3org/reva/pull/1946 \ No newline at end of file From e6a4768984c033c83969c28d88b8ec234d732f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 2 Aug 2021 19:10:16 +0200 Subject: [PATCH 3/3] fix license headers --- pkg/share/manager/sql/mocks/UserConverter.go | 18 ++++++++++++++++++ pkg/share/manager/sql/sql_suite_test.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/pkg/share/manager/sql/mocks/UserConverter.go b/pkg/share/manager/sql/mocks/UserConverter.go index 470ae2999b..ba7660c64d 100644 --- a/pkg/share/manager/sql/mocks/UserConverter.go +++ b/pkg/share/manager/sql/mocks/UserConverter.go @@ -1,3 +1,21 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + // Code generated by mockery v1.0.0. DO NOT EDIT. package mocks diff --git a/pkg/share/manager/sql/sql_suite_test.go b/pkg/share/manager/sql/sql_suite_test.go index 8d53a7c44a..a7e8fec950 100644 --- a/pkg/share/manager/sql/sql_suite_test.go +++ b/pkg/share/manager/sql/sql_suite_test.go @@ -1,3 +1,21 @@ +// 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_test import (