github.com/cs3org/reva/v2@v2.27.7/pkg/cbox/publicshare/sql/sql.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package sql
    20  
    21  import (
    22  	"context"
    23  	"database/sql"
    24  	"fmt"
    25  	"os"
    26  	"os/signal"
    27  	"strconv"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	"golang.org/x/crypto/bcrypt"
    33  
    34  	gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    35  	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    36  	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
    37  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    38  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    39  	conversions "github.com/cs3org/reva/v2/pkg/cbox/utils"
    40  	"github.com/cs3org/reva/v2/pkg/errtypes"
    41  	"github.com/cs3org/reva/v2/pkg/publicshare"
    42  	"github.com/cs3org/reva/v2/pkg/publicshare/manager/registry"
    43  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    44  	"github.com/cs3org/reva/v2/pkg/utils"
    45  	"github.com/mitchellh/mapstructure"
    46  	"github.com/pkg/errors"
    47  )
    48  
    49  const publicShareType = 3
    50  
    51  func init() {
    52  	registry.Register("sql", New)
    53  }
    54  
    55  type config struct {
    56  	SharePasswordHashCost      int    `mapstructure:"password_hash_cost"`
    57  	JanitorRunInterval         int    `mapstructure:"janitor_run_interval"`
    58  	EnableExpiredSharesCleanup bool   `mapstructure:"enable_expired_shares_cleanup"`
    59  	DbUsername                 string `mapstructure:"db_username"`
    60  	DbPassword                 string `mapstructure:"db_password"`
    61  	DbHost                     string `mapstructure:"db_host"`
    62  	DbPort                     int    `mapstructure:"db_port"`
    63  	DbName                     string `mapstructure:"db_name"`
    64  	GatewaySvc                 string `mapstructure:"gatewaysvc"`
    65  }
    66  
    67  type manager struct {
    68  	c      *config
    69  	db     *sql.DB
    70  	client gatewayv1beta1.GatewayAPIClient
    71  }
    72  
    73  func (c *config) init() {
    74  	if c.SharePasswordHashCost == 0 {
    75  		c.SharePasswordHashCost = 11
    76  	}
    77  	if c.JanitorRunInterval == 0 {
    78  		c.JanitorRunInterval = 3600
    79  	}
    80  }
    81  
    82  func (m *manager) startJanitorRun() {
    83  	if !m.c.EnableExpiredSharesCleanup {
    84  		return
    85  	}
    86  
    87  	ticker := time.NewTicker(time.Duration(m.c.JanitorRunInterval) * time.Second)
    88  	work := make(chan os.Signal, 1)
    89  	signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
    90  
    91  	for {
    92  		select {
    93  		case <-work:
    94  			return
    95  		case <-ticker.C:
    96  			_ = m.cleanupExpiredShares()
    97  		}
    98  	}
    99  }
   100  
   101  // New returns a new public share manager.
   102  func New(m map[string]interface{}) (publicshare.Manager, error) {
   103  	c := &config{}
   104  	if err := mapstructure.Decode(m, c); err != nil {
   105  		return nil, err
   106  	}
   107  	c.init()
   108  
   109  	db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	mgr := manager{
   120  		c:      c,
   121  		db:     db,
   122  		client: gw,
   123  	}
   124  	go mgr.startJanitorRun()
   125  
   126  	return &mgr, nil
   127  }
   128  
   129  func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
   130  
   131  	tkn := utils.RandString(15)
   132  	now := time.Now().Unix()
   133  
   134  	displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"]
   135  	if !ok {
   136  		displayName = tkn
   137  	}
   138  	createdAt := &typespb.Timestamp{
   139  		Seconds: uint64(now),
   140  	}
   141  
   142  	creator := conversions.FormatUserID(u.Id)
   143  	owner := conversions.FormatUserID(rInfo.Owner)
   144  	permissions := conversions.SharePermToInt(g.Permissions.Permissions)
   145  	itemType := conversions.ResourceTypeToItem(rInfo.Type)
   146  	prefix := rInfo.Id.SpaceId
   147  	itemSource := rInfo.Id.OpaqueId
   148  	fileSource, err := strconv.ParseUint(itemSource, 10, 64)
   149  	if err != nil {
   150  		// it can be the case that the item source may be a character string
   151  		// we leave fileSource blank in that case
   152  		fileSource = 0
   153  	}
   154  
   155  	query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?"
   156  	params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName}
   157  
   158  	var passwordProtected bool
   159  	password := g.Password
   160  	if password != "" {
   161  		password, err = hashPassword(password, m.c.SharePasswordHashCost)
   162  		if err != nil {
   163  			return nil, errors.Wrap(err, "could not hash share password")
   164  		}
   165  		passwordProtected = true
   166  
   167  		query += ",share_with=?"
   168  		params = append(params, password)
   169  	}
   170  
   171  	if g.Expiration != nil && g.Expiration.Seconds != 0 {
   172  		t := time.Unix(int64(g.Expiration.Seconds), 0)
   173  		query += ",expiration=?"
   174  		params = append(params, t)
   175  	}
   176  
   177  	stmt, err := m.db.Prepare(query)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	result, err := stmt.Exec(params...)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	lastID, err := result.LastInsertId()
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	return &link.PublicShare{
   191  		Id: &link.PublicShareId{
   192  			OpaqueId: strconv.FormatInt(lastID, 10),
   193  		},
   194  		Owner:             rInfo.GetOwner(),
   195  		Creator:           u.Id,
   196  		ResourceId:        rInfo.Id,
   197  		Token:             tkn,
   198  		Permissions:       g.Permissions,
   199  		Ctime:             createdAt,
   200  		Mtime:             createdAt,
   201  		PasswordProtected: passwordProtected,
   202  		Expiration:        g.Expiration,
   203  		DisplayName:       displayName,
   204  	}, nil
   205  }
   206  
   207  func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
   208  	query := "update oc_share set "
   209  	paramsMap := map[string]interface{}{}
   210  	params := []interface{}{}
   211  
   212  	now := time.Now().Unix()
   213  	uid := conversions.FormatUserID(u.Id)
   214  
   215  	switch req.GetUpdate().GetType() {
   216  	case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
   217  		paramsMap["share_name"] = req.Update.GetDisplayName()
   218  	case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
   219  		paramsMap["permissions"] = conversions.SharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
   220  	case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
   221  		paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
   222  	case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
   223  		if req.Update.GetGrant().Password == "" {
   224  			paramsMap["share_with"] = ""
   225  		} else {
   226  			h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
   227  			if err != nil {
   228  				return nil, errors.Wrap(err, "could not hash share password")
   229  			}
   230  			paramsMap["share_with"] = h
   231  		}
   232  	default:
   233  		return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
   234  	}
   235  
   236  	for k, v := range paramsMap {
   237  		query += k + "=?"
   238  		params = append(params, v)
   239  	}
   240  
   241  	switch {
   242  	case req.Ref.GetId() != nil:
   243  		query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
   244  		params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
   245  	case req.Ref.GetToken() != "":
   246  		query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
   247  		params = append(params, now, req.Ref.GetToken(), uid, uid)
   248  	default:
   249  		return nil, errtypes.NotFound(req.Ref.String())
   250  	}
   251  
   252  	stmt, err := m.db.Prepare(query)
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	if _, err = stmt.Exec(params...); err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	return m.GetPublicShare(ctx, u, req.Ref, false)
   261  }
   262  
   263  func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) {
   264  	s := conversions.DBShare{Token: token}
   265  	query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?"
   266  	if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
   267  		if err == sql.ErrNoRows {
   268  			return nil, "", errtypes.NotFound(token)
   269  		}
   270  		return nil, "", err
   271  	}
   272  	share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
   273  	if err != nil {
   274  		return nil, "", err
   275  	}
   276  	return share, s.ShareWith, nil
   277  }
   278  
   279  func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) {
   280  	uid := conversions.FormatUserID(u.Id)
   281  	s := conversions.DBShare{ID: id.OpaqueId}
   282  	query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)"
   283  	if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions); err != nil {
   284  		if err == sql.ErrNoRows {
   285  			return nil, "", errtypes.NotFound(id.OpaqueId)
   286  		}
   287  		return nil, "", err
   288  	}
   289  	share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
   290  	if err != nil {
   291  		return nil, "", err
   292  	}
   293  	return share, s.ShareWith, nil
   294  }
   295  
   296  func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
   297  	var s *link.PublicShare
   298  	var pw string
   299  	var err error
   300  	switch {
   301  	case ref.GetId() != nil:
   302  		s, pw, err = m.getByID(ctx, ref.GetId(), u)
   303  	case ref.GetToken() != "":
   304  		s, pw, err = m.getByToken(ctx, ref.GetToken(), u)
   305  	default:
   306  		err = errtypes.NotFound(ref.String())
   307  	}
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  
   312  	if expired(s) {
   313  		if err := m.cleanupExpiredShares(); err != nil {
   314  			return nil, err
   315  		}
   316  		return nil, errtypes.NotFound(ref.String())
   317  	}
   318  
   319  	if s.PasswordProtected && sign {
   320  		if err := publicshare.AddSignature(s, pw); err != nil {
   321  			return nil, err
   322  		}
   323  
   324  	}
   325  
   326  	return s, nil
   327  }
   328  
   329  func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
   330  	uid := conversions.FormatUserID(u.Id)
   331  	query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=?)"
   332  	var resourceFilters, ownerFilters, creatorFilters string
   333  	var resourceParams, ownerParams, creatorParams []interface{}
   334  	params := []interface{}{uid, uid, publicShareType}
   335  
   336  	for _, f := range filters {
   337  		switch f.Type {
   338  		case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
   339  			if len(resourceFilters) != 0 {
   340  				resourceFilters += " OR "
   341  			}
   342  			resourceFilters += "(fileid_prefix=? AND item_source=?)"
   343  			resourceParams = append(resourceParams, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
   344  		case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
   345  			if len(ownerFilters) != 0 {
   346  				ownerFilters += " OR "
   347  			}
   348  			ownerFilters += "(uid_owner=?)"
   349  			ownerParams = append(ownerParams, conversions.FormatUserID(f.GetOwner()))
   350  		case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
   351  			if len(creatorFilters) != 0 {
   352  				creatorFilters += " OR "
   353  			}
   354  			creatorFilters += "(uid_initiator=?)"
   355  			creatorParams = append(creatorParams, conversions.FormatUserID(f.GetCreator()))
   356  		}
   357  	}
   358  	if resourceFilters != "" {
   359  		query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
   360  		params = append(params, resourceParams...)
   361  	}
   362  	if ownerFilters != "" {
   363  		query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
   364  		params = append(params, ownerParams...)
   365  	}
   366  	if creatorFilters != "" {
   367  		query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
   368  		params = append(params, creatorParams...)
   369  	}
   370  
   371  	rows, err := m.db.Query(query, params...)
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  	defer rows.Close()
   376  
   377  	var s conversions.DBShare
   378  	shares := []*link.PublicShare{}
   379  	for rows.Next() {
   380  		if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
   381  			continue
   382  		}
   383  		cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  		if expired(cs3Share) {
   388  			_ = m.cleanupExpiredShares()
   389  		} else {
   390  			if cs3Share.PasswordProtected && sign {
   391  				if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
   392  					return nil, err
   393  				}
   394  			}
   395  			shares = append(shares, cs3Share)
   396  		}
   397  	}
   398  	if err = rows.Err(); err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	return shares, nil
   403  }
   404  
   405  func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
   406  	uid := conversions.FormatUserID(u.Id)
   407  	query := "delete from oc_share where "
   408  	params := []interface{}{}
   409  
   410  	switch {
   411  	case ref.GetId() != nil && ref.GetId().OpaqueId != "":
   412  		query += "id=? AND (uid_owner=? or uid_initiator=?)"
   413  		params = append(params, ref.GetId().OpaqueId, uid, uid)
   414  	case ref.GetToken() != "":
   415  		query += "token=? AND (uid_owner=? or uid_initiator=?)"
   416  		params = append(params, ref.GetToken(), uid, uid)
   417  	default:
   418  		return errtypes.NotFound(ref.String())
   419  	}
   420  
   421  	stmt, err := m.db.Prepare(query)
   422  	if err != nil {
   423  		return err
   424  	}
   425  	res, err := stmt.Exec(params...)
   426  	if err != nil {
   427  		return err
   428  	}
   429  
   430  	rowCnt, err := res.RowsAffected()
   431  	if err != nil {
   432  		return err
   433  	}
   434  	if rowCnt == 0 {
   435  		return errtypes.NotFound(ref.String())
   436  	}
   437  	return nil
   438  }
   439  
   440  func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
   441  	s := conversions.DBShare{Token: token}
   442  	query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?"
   443  	if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
   444  		if err == sql.ErrNoRows {
   445  			return nil, errtypes.NotFound(token)
   446  		}
   447  		return nil, err
   448  	}
   449  	cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	if s.ShareWith != "" {
   454  		if !authenticate(cs3Share, s.ShareWith, auth) {
   455  			// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
   456  			return nil, errtypes.InvalidCredentials(token)
   457  		}
   458  
   459  		if sign {
   460  			if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
   461  				return nil, err
   462  			}
   463  		}
   464  	}
   465  
   466  	if expired(cs3Share) {
   467  		if err := m.cleanupExpiredShares(); err != nil {
   468  			return nil, err
   469  		}
   470  		return nil, errtypes.NotFound(token)
   471  	}
   472  
   473  	return cs3Share, nil
   474  }
   475  
   476  func (m *manager) cleanupExpiredShares() error {
   477  	if !m.c.EnableExpiredSharesCleanup {
   478  		return nil
   479  	}
   480  
   481  	query := "update oc_share set orphan = 1 where expiration IS NOT NULL AND expiration < ?"
   482  	params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
   483  
   484  	stmt, err := m.db.Prepare(query)
   485  	if err != nil {
   486  		return err
   487  	}
   488  	if _, err = stmt.Exec(params...); err != nil {
   489  		return err
   490  	}
   491  	return nil
   492  }
   493  
   494  func expired(s *link.PublicShare) bool {
   495  	if s.Expiration != nil {
   496  		if t := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())); t.Before(time.Now()) {
   497  			return true
   498  		}
   499  	}
   500  	return false
   501  }
   502  
   503  func hashPassword(password string, cost int) (string, error) {
   504  	bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
   505  	return "1|" + string(bytes), err
   506  }
   507  
   508  func checkPasswordHash(password, hash string) bool {
   509  	err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password))
   510  	return err == nil
   511  }
   512  
   513  func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
   514  	switch {
   515  	case auth.GetPassword() != "":
   516  		return checkPasswordHash(auth.GetPassword(), pw)
   517  	case auth.GetSignature() != nil:
   518  		sig := auth.GetSignature()
   519  		now := time.Now()
   520  		expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos()))
   521  		if now.After(expiration) {
   522  			return false
   523  		}
   524  		s, err := publicshare.CreateSignature(share.Token, pw, expiration)
   525  		if err != nil {
   526  			// TODO(labkode): pass context to call to log err.
   527  			// No we are blind
   528  			return false
   529  		}
   530  		return sig.GetSignature() == s
   531  	}
   532  	return false
   533  }