From 6a99621a5e68aa43d5ab5b64385dc411964e5e3b Mon Sep 17 00:00:00 2001 From: Ishank Arora Date: Fri, 2 Oct 2020 11:47:56 +0200 Subject: [PATCH] SQL driver for share manager SQL driver for share manager --- pkg/share/manager/json/json.go | 6 +- pkg/share/manager/loader/loader.go | 1 + pkg/share/manager/sql/conversions.go | 164 ++++++++++ pkg/share/manager/sql/sql.go | 461 +++++++++++++++++++++++++++ 4 files changed, 628 insertions(+), 4 deletions(-) create mode 100644 pkg/share/manager/sql/conversions.go create mode 100644 pkg/share/manager/sql/sql.go diff --git a/pkg/share/manager/json/json.go b/pkg/share/manager/json/json.go index af390fe8111..60b4170fbac 100644 --- a/pkg/share/manager/json/json.go +++ b/pkg/share/manager/json/json.go @@ -61,12 +61,10 @@ func New(m map[string]interface{}) (share.Manager, error) { return nil, err } - mgr := &mgr{ + return &mgr{ c: c, model: model, - } - - return mgr, nil + }, nil } func loadOrCreate(file string) (*shareModel, error) { diff --git a/pkg/share/manager/loader/loader.go b/pkg/share/manager/loader/loader.go index 58e33603eb6..3b7215f2c08 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..85dbdf21551 --- /dev/null +++ b/pkg/share/manager/sql/conversions.go @@ -0,0 +1,164 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql + +import ( + "fmt" + "strconv" + "strings" + + 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/go-sql-driver/mysql" +) + +func granteeTypeToInt(g provider.GranteeType) int { + switch g { + case provider.GranteeType_GRANTEE_TYPE_USER: + return 0 + case provider.GranteeType_GRANTEE_TYPE_GROUP: + return 1 + default: + return -1 + } +} + +func intToGranteeType(g int) provider.GranteeType { + switch g { + case 0: + return provider.GranteeType_GRANTEE_TYPE_USER + case 1: + return provider.GranteeType_GRANTEE_TYPE_GROUP + default: + return provider.GranteeType_GRANTEE_TYPE_INVALID + } +} + +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 itemToresourceType(r string) provider.ResourceType { + switch r { + case "file": + return provider.ResourceType_RESOURCE_TYPE_FILE + case "folder": + return provider.ResourceType_RESOURCE_TYPE_CONTAINER + case "reference": + return provider.ResourceType_RESOURCE_TYPE_REFERENCE + case "symlink": + return provider.ResourceType_RESOURCE_TYPE_SYMLINK + default: + return provider.ResourceType_RESOURCE_TYPE_INVALID + } +} + +func sharePermToInt(p *provider.ResourcePermissions) int { + var perm int + if p.CreateContainer { + perm = 15 + } + if p.ListContainer { + perm = 1 + } + return perm +} + +func intTosharePerm(p int) *provider.ResourcePermissions { + switch p { + case 1: + return &provider.ResourcePermissions{ + ListContainer: true, + ListGrants: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + } + case 15: + return &provider.ResourcePermissions{ + ListContainer: true, + ListGrants: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + + Move: true, + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + CreateContainer: true, + Delete: true, + PurgeRecycle: true, + } + default: + return &provider.ResourcePermissions{} + } +} + +func formatUserID(u *userpb.UserId) string { + if u.Idp != "" { + return fmt.Sprintf("%s:%s", u.OpaqueId, u.Idp) + } + return u.OpaqueId +} + +func extractUserID(u string) *userpb.UserId { + parts := strings.Split(u, ":") + if len(parts) > 1 { + return &userpb.UserId{OpaqueId: parts[0], Idp: parts[1]} + } + return &userpb.UserId{OpaqueId: parts[0]} +} + +func convertToCS3Share(s dbShare) *collaboration.Share { + ts := &typespb.Timestamp{ + Seconds: uint64(s.STime), + } + return &collaboration.Share{ + Id: &collaboration.ShareId{ + OpaqueId: strconv.Itoa(s.ID), + }, + ResourceId: &provider.ResourceId{OpaqueId: s.ItemSource, StorageId: s.Prefix}, + Permissions: &collaboration.SharePermissions{Permissions: intTosharePerm(s.Permissions)}, + Grantee: &provider.Grantee{Type: intToGranteeType(s.ShareType), Id: extractUserID(s.ShareWith)}, + Owner: extractUserID(s.UIDOwner), + Creator: extractUserID(s.UIDInitiator), + Ctime: ts, + Mtime: ts, + } +} diff --git a/pkg/share/manager/sql/sql.go b/pkg/share/manager/sql/sql.go new file mode 100644 index 00000000000..38bbbb1d3c2 --- /dev/null +++ b/pkg/share/manager/sql/sql.go @@ -0,0 +1,461 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sql + +import ( + "context" + "database/sql" + "fmt" + "path" + "reflect" + "strconv" + "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/go-sql-driver/mysql" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + "github.com/cs3org/reva/pkg/share/manager/registry" + "github.com/cs3org/reva/pkg/user" +) + +func init() { + registry.Register("json", New) +} + +type config struct { + dbUsername string `mapstructure:"db_username"` + dbPassword string `mapstructure:"db_password"` + dbHost string `mapstructure:"db_host"` + dbPort string `mapstructure:"db_port"` + dbName string `mapstructure:"db_name"` +} + +type mgr struct { + c *config + db *sql.DB +} + +type dbShare struct { + ID int + UIDOwner string + UIDInitiator string + Prefix string + ItemSource string + ShareWith string + Permissions int + ShareType int + STime int + FileTarget string + State int +} + +// 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 + } + + 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 + } + + return &mgr{ + c: c, + db: db, + }, nil +} + +func (c *config) init() { +} + +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 && + ((g.Grantee.Id.Idp == user.Id.Idp && g.Grantee.Id.OpaqueId == user.Id.OpaqueId) || + (g.Grantee.Id.Idp == md.Owner.Idp && g.Grantee.Id.OpaqueId == md.Owner.OpaqueId)) { + return nil, errors.New("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, + } + _, 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 := granteeTypeToInt(g.Grantee.Type) + itemType := resourceTypeToItem(md.Type) + targetPath := path.Join("/", path.Base(md.Path)) + permissions := sharePermToInt(g.Permissions.Permissions) + prefix := md.Id.StorageId + itemSource := md.Id.OpaqueId + fileSource, err := strconv.ParseUint(itemSource, 10, 64) + if err != nil { + return nil, err + } + + 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, formatUserID(md.Owner), formatUserID(user.Id), itemType, prefix, itemSource, fileSource, permissions, now, formatUserID(g.Grantee.Id), 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) { + intID, err := strconv.ParseInt(id.OpaqueId, 10, 64) + if err != nil { + return nil, err + } + + s := dbShare{ID: int(intID)} + 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, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND id=?" + if err := m.db.QueryRow(query, id.OpaqueId).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.STime, &s.Permissions, &s.ShareType); err != nil { + if err == sql.ErrNoRows { + return nil, errtypes.NotFound(id.OpaqueId) + } + return nil, err + } + return convertToCS3Share(s), nil +} + +func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) { + s := 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, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=?" + if err := m.db.QueryRow(query, formatUserID(key.Owner), formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, granteeTypeToInt(key.Grantee.Type), formatUserID(key.Grantee.Id)).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &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 convertToCS3Share(s), nil +} + +func (m *mgr) get(ctx context.Context, ref *collaboration.ShareReference) (s *collaboration.Share, 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 + } + + // check if we are the owner + user := user.ContextMustGetUser(ctx) + if (user.Id.Idp == s.Owner.Idp && user.Id.OpaqueId == s.Owner.OpaqueId) || + (user.Id.Idp == s.Creator.Idp && user.Id.OpaqueId == s.Creator.OpaqueId) { + return s, nil + } + + // we return not found to not disclose information + return nil, errtypes.NotFound(ref.String()) +} + +func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) { + share, err := m.get(ctx, ref) + if err != nil { + return nil, err + } + + return share, nil +} + +func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error { + user := user.ContextMustGetUser(ctx) + + 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, formatUserID(user.Id), formatUserID(user.Id)) + case ref.GetKey() != nil: + key := ref.GetKey() + if key.Owner != user.Id { + return errtypes.PermissionDenied(ref.String()) + } + query = "delete from oc_share where (uid_owner=? or uid_initiator=?) AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=?" + params = append(params, formatUserID(key.Owner), formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, granteeTypeToInt(key.Grantee.Type), formatUserID(key.Grantee.Id)) + 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 +} + +// TODO(labkode): this is fragile, the check should be done on primitive types. +func equal(ref *collaboration.ShareReference, s *collaboration.Share) bool { + if ref.GetId() != nil && s.Id != nil { + if ref.GetId().OpaqueId == s.Id.OpaqueId { + return true + } + } else if ref.GetKey() != nil { + if (reflect.DeepEqual(*ref.GetKey().Owner, *s.Owner) || reflect.DeepEqual(*ref.GetKey().Owner, *s.Creator)) && + reflect.DeepEqual(*ref.GetKey().ResourceId, *s.ResourceId) && reflect.DeepEqual(*ref.GetKey().Grantee, *s.Grantee) { + return true + } + } + return false +} + +func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions) (*collaboration.Share, error) { + user := user.ContextMustGetUser(ctx) + + 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, ref.GetId().OpaqueId, formatUserID(user.Id), formatUserID(user.Id)) + case ref.GetKey() != nil: + key := ref.GetKey() + if key.Owner != user.Id { + return nil, errtypes.PermissionDenied(ref.String()) + } + 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=?" + params = append(params, formatUserID(key.Owner), formatUserID(key.Owner), key.ResourceId.StorageId, key.ResourceId.OpaqueId, granteeTypeToInt(key.Grantee.Type), formatUserID(key.Grantee.Id)) + 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) { + 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, 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=?)" + var filterQuery string + params := []interface{}{formatUserID(user.ContextMustGetUser(ctx).Id), 0, 1} + for i, f := range filters { + if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + filterQuery += "(fileid_prefix=? AND item_source=?)" + if i != len(filters)-1 { + filterQuery += " OR " + } + params = append(params, f.GetResourceId().StorageId, f.GetResourceId().OpaqueId) + } + } + 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.Prefix, &s.ItemSource, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { + continue + } + shares = append(shares, convertToCS3Share(s)) + } + 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) { + var rss []*collaboration.ReceivedShare + m.Lock() + defer m.Unlock() + user := user.ContextMustGetUser(ctx) + for _, s := range m.model.Shares { + if (user.Id.Idp == s.Owner.Idp && user.Id.OpaqueId == s.Owner.OpaqueId) || + (user.Id.Idp == s.Creator.Idp && user.Id.OpaqueId == s.Creator.OpaqueId) { + // omit shares created by me + continue + } + if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER { + if user.Id.Idp == s.Grantee.Id.Idp && user.Id.OpaqueId == s.Grantee.Id.OpaqueId { + rs := m.convert(ctx, s) + rss = append(rss, rs) + } + } else if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + // check if all user groups match this share; TODO(labkode): filter shares created by us. + for _, g := range user.Groups { + if g == s.Grantee.Id.OpaqueId { + rs := m.convert(ctx, s) + rss = append(rss, rs) + } + } + } + } + return rss, nil +} + +// convert must be called in a lock-controlled block. +func (m *mgr) convert(ctx context.Context, s *collaboration.Share) *collaboration.ReceivedShare { + rs := &collaboration.ReceivedShare{ + Share: s, + State: collaboration.ShareState_SHARE_STATE_PENDING, + } + user := user.ContextMustGetUser(ctx) + if v, ok := m.model.State[user.Id.String()]; ok { + if state, ok := v[s.Id.String()]; ok { + rs.State = state + } + } + 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() + user := user.ContextMustGetUser(ctx) + for _, s := range m.model.Shares { + if equal(ref, s) { + if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && + s.Grantee.Id.Idp == user.Id.Idp && s.Grantee.Id.OpaqueId == user.Id.OpaqueId { + rs := m.convert(ctx, s) + return rs, nil + } else if s.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + for _, g := range user.Groups { + if s.Grantee.Id.OpaqueId == g { + rs := m.convert(ctx, s) + return rs, nil + } + } + } + } + } + return nil, errtypes.NotFound(ref.String()) +} + +func (m *mgr) UpdateReceivedShare(ctx context.Context, ref *collaboration.ShareReference, f *collaboration.UpdateReceivedShareRequest_UpdateField) (*collaboration.ReceivedShare, error) { + rs, err := m.getReceived(ctx, ref) + if err != nil { + return nil, err + } + + user := user.ContextMustGetUser(ctx) + m.Lock() + defer m.Unlock() + + if v, ok := m.model.State[user.Id.String()]; ok { + v[rs.Share.Id.String()] = f.GetState() + m.model.State[user.Id.String()] = v + } else { + a := map[string]collaboration.ShareState{ + rs.Share.Id.String(): f.GetState(), + } + m.model.State[user.Id.String()] = a + } + + if err := m.model.Save(); err != nil { + err = errors.Wrap(err, "error saving model") + return nil, err + } + + return rs, nil +}