diff --git a/pkg/share/manager/loader/loader.go b/pkg/share/manager/loader/loader.go index 46f7a057c27..b3ebbcae29c 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 00000000000..a366ac269b3 --- /dev/null +++ b/pkg/share/manager/sql/conversions.go @@ -0,0 +1,259 @@ +// 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 + 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 00000000000..470ae2999bd --- /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 00000000000..6a97f4321ca --- /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("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=0 where id=?" + case collaboration.ShareState_SHARE_STATE_ACCEPTED: + queryAccept = "update oc_share set accepted=1 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 00000000000..8d53a7c44ac --- /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 00000000000..bb4dffcdcea --- /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 00000000000..fba76fdcc4f Binary files /dev/null and b/pkg/share/manager/sql/test.db differ