github.com/cs3org/reva/v2@v2.27.7/pkg/publicshare/manager/owncloudsql/owncloudsql.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 owncloudsql implements a publiclink share manager backed by an existing ownCloud 10 database
    20  //
    21  // The SQL queries use `coalesce({column_identifier}, ”) as {column_identifier}` to read an emptystring
    22  // instead of null values, which better fits the golang default values.
    23  package owncloudsql
    24  
    25  import (
    26  	"context"
    27  	"database/sql"
    28  	"fmt"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    34  	link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
    35  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    36  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    37  	"github.com/cs3org/reva/v2/pkg/errtypes"
    38  	"github.com/cs3org/reva/v2/pkg/publicshare"
    39  	"github.com/cs3org/reva/v2/pkg/publicshare/manager/registry"
    40  	"github.com/cs3org/reva/v2/pkg/sharedconf"
    41  	"github.com/cs3org/reva/v2/pkg/utils"
    42  	"github.com/mitchellh/mapstructure"
    43  	"github.com/pkg/errors"
    44  	"golang.org/x/crypto/bcrypt"
    45  	"google.golang.org/protobuf/proto"
    46  
    47  	// Provides mysql drivers
    48  	_ "github.com/go-sql-driver/mysql"
    49  )
    50  
    51  const (
    52  	publicShareType = 3
    53  )
    54  
    55  func init() {
    56  	registry.Register("owncloudsql", NewMysql)
    57  }
    58  
    59  // Config configures an owncloudsql publicshare manager
    60  type Config struct {
    61  	GatewayAddr                string `mapstructure:"gateway_addr"`
    62  	DbUsername                 string `mapstructure:"db_username"`
    63  	DbPassword                 string `mapstructure:"db_password"`
    64  	DbHost                     string `mapstructure:"db_host"`
    65  	DbPort                     int    `mapstructure:"db_port"`
    66  	DbName                     string `mapstructure:"db_name"`
    67  	EnableExpiredSharesCleanup bool   `mapstructure:"enable_expired_shares_cleanup"`
    68  	SharePasswordHashCost      int    `mapstructure:"password_hash_cost"`
    69  }
    70  
    71  type mgr struct {
    72  	driver        string
    73  	db            *sql.DB
    74  	c             Config
    75  	userConverter UserConverter
    76  }
    77  
    78  // NewMysql returns a new publicshare manager connection to a mysql database
    79  func NewMysql(m map[string]interface{}) (publicshare.Manager, error) {
    80  	c, err := parseConfig(m)
    81  	if err != nil {
    82  		err = errors.Wrap(err, "error creating a new manager")
    83  		return nil, err
    84  	}
    85  
    86  	db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.DbUsername, c.DbPassword, c.DbHost, c.DbPort, c.DbName))
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	userConverter := NewGatewayUserConverter(sharedconf.GetGatewaySVC(c.GatewayAddr))
    92  
    93  	return New("mysql", db, *c, userConverter)
    94  }
    95  
    96  // New returns a new Cache instance connecting to the given sql.DB
    97  func New(driver string, db *sql.DB, c Config, userConverter UserConverter) (publicshare.Manager, error) {
    98  	if c.SharePasswordHashCost == 0 {
    99  		c.SharePasswordHashCost = bcrypt.DefaultCost
   100  	}
   101  	return &mgr{
   102  		driver:        driver,
   103  		db:            db,
   104  		c:             c,
   105  		userConverter: userConverter,
   106  	}, nil
   107  }
   108  
   109  func parseConfig(m map[string]interface{}) (*Config, error) {
   110  	c := &Config{}
   111  	if err := mapstructure.Decode(m, c); err != nil {
   112  		return nil, err
   113  	}
   114  	return c, nil
   115  }
   116  
   117  func (m *mgr) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
   118  
   119  	tkn := utils.RandString(15)
   120  	now := time.Now().Unix()
   121  
   122  	displayName := tkn
   123  	if rInfo.ArbitraryMetadata != nil && rInfo.ArbitraryMetadata.Metadata["name"] != "" {
   124  		displayName = rInfo.ArbitraryMetadata.Metadata["name"]
   125  	}
   126  	createdAt := &typespb.Timestamp{
   127  		Seconds: uint64(now),
   128  	}
   129  
   130  	creator := u.Username
   131  	owner, err := m.userConverter.UserIDToUserName(ctx, rInfo.Owner)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	permissions := sharePermToInt(g.Permissions.Permissions)
   136  	itemType := resourceTypeToItem(rInfo.Type)
   137  
   138  	itemSource := rInfo.Id.OpaqueId
   139  	fileSource, err := strconv.ParseUint(itemSource, 10, 64)
   140  	if err != nil {
   141  		// it can be the case that the item source may be a character string
   142  		// we leave fileSource blank in that case
   143  		fileSource = 0
   144  	}
   145  
   146  	columns := "share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,token,share_name"
   147  	placeholders := "?,?,?,?,?,?,?,?,?,?"
   148  	params := []interface{}{publicShareType, owner, creator, itemType, itemSource, fileSource, permissions, now, tkn, displayName}
   149  
   150  	var passwordProtected bool
   151  	password := g.Password
   152  	if password != "" {
   153  		password, err = hashPassword(password, m.c.SharePasswordHashCost)
   154  		if err != nil {
   155  			return nil, errors.Wrap(err, "could not hash share password")
   156  		}
   157  		passwordProtected = true
   158  
   159  		columns += ",share_with"
   160  		placeholders += ",?"
   161  		params = append(params, password)
   162  	}
   163  
   164  	if g.Expiration != nil && g.Expiration.Seconds != 0 {
   165  		t := time.Unix(int64(g.Expiration.Seconds), 0)
   166  		columns += ",expiration"
   167  		placeholders += ",?"
   168  		params = append(params, t)
   169  	}
   170  
   171  	query := "INSERT INTO oc_share (" + columns + ") VALUES (" + placeholders + ")"
   172  	stmt, err := m.db.Prepare(query)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	result, err := stmt.Exec(params...)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	lastID, err := result.LastInsertId()
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	return &link.PublicShare{
   186  		Id: &link.PublicShareId{
   187  			OpaqueId: strconv.FormatInt(lastID, 10),
   188  		},
   189  		Owner:             rInfo.GetOwner(),
   190  		Creator:           u.Id,
   191  		ResourceId:        rInfo.Id,
   192  		Token:             tkn,
   193  		Permissions:       g.Permissions,
   194  		Ctime:             createdAt,
   195  		Mtime:             createdAt,
   196  		PasswordProtected: passwordProtected,
   197  		Expiration:        g.Expiration,
   198  		DisplayName:       displayName,
   199  	}, nil
   200  }
   201  
   202  // owncloud 10 prefixes the hash with `1|`
   203  func hashPassword(password string, cost int) (string, error) {
   204  	bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
   205  	return "1|" + string(bytes), err
   206  }
   207  
   208  // UpdatePublicShare updates the expiration date, permissions and Mtime
   209  func (m *mgr) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
   210  	query := "update oc_share set "
   211  	paramsMap := map[string]interface{}{}
   212  	params := []interface{}{}
   213  
   214  	now := time.Now().Unix()
   215  	uid := u.Username
   216  
   217  	switch req.GetUpdate().GetType() {
   218  	case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
   219  		paramsMap["share_name"] = req.Update.GetDisplayName()
   220  	case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
   221  		paramsMap["permissions"] = sharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
   222  	case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
   223  		paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
   224  	case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
   225  		if req.Update.GetGrant().Password == "" {
   226  			paramsMap["share_with"] = ""
   227  		} else {
   228  			h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
   229  			if err != nil {
   230  				return nil, errors.Wrap(err, "could not hash share password")
   231  			}
   232  			paramsMap["share_with"] = h
   233  		}
   234  	default:
   235  		return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
   236  	}
   237  
   238  	for k, v := range paramsMap {
   239  		query += k + "=?"
   240  		params = append(params, v)
   241  	}
   242  
   243  	switch {
   244  	case req.Ref.GetId() != nil:
   245  		query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
   246  		params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
   247  	case req.Ref.GetToken() != "":
   248  		query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
   249  		params = append(params, now, req.Ref.GetToken(), uid, uid)
   250  	default:
   251  		return nil, errtypes.NotFound(req.Ref.String())
   252  	}
   253  
   254  	stmt, err := m.db.Prepare(query)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  	if _, err = stmt.Exec(params...); err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	return m.GetPublicShare(ctx, u, req.Ref, false)
   263  }
   264  
   265  func (m *mgr) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
   266  
   267  	ps, err := m.getWithPassword(ctx, ref)
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	if publicshare.IsExpired(&ps.PublicShare) {
   273  		if err := m.cleanupExpiredShares(); err != nil {
   274  			return nil, err
   275  		}
   276  		return nil, errtypes.NotFound("public share has expired")
   277  	}
   278  
   279  	if ps.PublicShare.PasswordProtected && sign {
   280  		err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
   281  		if err != nil {
   282  			return nil, err
   283  		}
   284  	}
   285  
   286  	return &ps.PublicShare, nil
   287  }
   288  
   289  func (m *mgr) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
   290  	switch {
   291  	case ref.GetToken() != "":
   292  		return m.getByToken(ctx, ref.GetToken())
   293  	case ref.GetId().GetOpaqueId() != "":
   294  		return m.getByID(ctx, ref.GetId().GetOpaqueId())
   295  	default:
   296  		return nil, errtypes.BadRequest("neither id nor token given")
   297  	}
   298  }
   299  
   300  func (m *mgr) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
   301  	s, err := getByToken(m.db, token)
   302  	if err != nil {
   303  		return nil, err
   304  	}
   305  	ps, err := m.ConvertToCS3PublicShare(ctx, s)
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	ret := &publicshare.WithPassword{
   310  		Password: strings.TrimPrefix(s.ShareWith, "1|"),
   311  	}
   312  	proto.Merge(&ret.PublicShare, ps)
   313  	return ret, nil
   314  }
   315  
   316  func getByToken(db *sql.DB, token string) (DBShare, error) {
   317  	s := DBShare{Token: token}
   318  	query := `SELECT
   319  				coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
   320  				coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
   321  				coalesce(item_type, '') as item_type,
   322  				coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
   323  				s.id, s.stime, s.permissions, fc.storage as storage
   324  			FROM oc_share s
   325  			LEFT JOIN oc_filecache fc ON fc.fileid = file_source
   326  			WHERE share_type=? AND token=?`
   327  	if err := db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
   328  		if err == sql.ErrNoRows {
   329  			return s, errtypes.NotFound(token)
   330  		}
   331  		return s, err
   332  	}
   333  	return s, nil
   334  }
   335  
   336  func (m *mgr) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
   337  	s := DBShare{ID: id}
   338  	query := `SELECT
   339  				coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
   340  				coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
   341  				coalesce(item_type, '') as item_type, coalesce(token,'') as token,
   342  				coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
   343  				s.stime, s.permissions, fc.storage as storage
   344  			FROM oc_share s
   345  			LEFT JOIN oc_filecache fc ON fc.fileid = file_source
   346  			WHERE share_type=? AND id=?`
   347  	if err := m.db.QueryRow(query, publicShareType, id).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
   348  		if err == sql.ErrNoRows {
   349  			return nil, errtypes.NotFound(id)
   350  		}
   351  		return nil, err
   352  	}
   353  	ps, err := m.ConvertToCS3PublicShare(ctx, s)
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	ret := &publicshare.WithPassword{
   358  		Password: strings.TrimPrefix(s.ShareWith, "1|"),
   359  	}
   360  	proto.Merge(&ret.PublicShare, ps)
   361  	return ret, nil
   362  }
   363  
   364  func (m *mgr) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
   365  	uid := u.Username
   366  	// FIXME instead of joining we may want to have to do a stat call ... if we want to store shares from other providers? or just Dump()? and be done with migration?
   367  	query := `SELECT
   368  				coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, 
   369  				coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
   370  				coalesce(item_type, '') as item_type, coalesce(token,'') as token,
   371  				coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
   372  				s.id, s.stime, s.permissions, fc.storage as storage
   373  			FROM oc_share s
   374  			LEFT JOIN oc_filecache fc ON fc.fileid = file_source
   375  			WHERE (uid_owner=? or uid_initiator=?)
   376  			AND (share_type=?)`
   377  	var resourceFilters, ownerFilters, creatorFilters, storageFilters string
   378  	var resourceParams, ownerParams, creatorParams, storageParams []interface{}
   379  	params := []interface{}{uid, uid, publicShareType}
   380  
   381  	for _, f := range filters {
   382  		switch f.Type {
   383  		case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
   384  			if len(resourceFilters) != 0 {
   385  				resourceFilters += " OR "
   386  			}
   387  			resourceFilters += "item_source=?"
   388  			resourceParams = append(resourceParams, f.GetResourceId().GetOpaqueId())
   389  		case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
   390  			if len(ownerFilters) != 0 {
   391  				ownerFilters += " OR "
   392  			}
   393  			ownerFilters += "(uid_owner=?)"
   394  			ownerParams = append(ownerParams, formatUserID(f.GetOwner()))
   395  		case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
   396  			if len(creatorFilters) != 0 {
   397  				creatorFilters += " OR "
   398  			}
   399  			creatorFilters += "(uid_initiator=?)"
   400  			creatorParams = append(creatorParams, formatUserID(f.GetCreator()))
   401  		case publicshare.StorageIDFilterType:
   402  			if len(storageFilters) != 0 {
   403  				storageFilters += " OR "
   404  			}
   405  			storageFilters += "(storage=?)"
   406  			storageParams = append(storageParams, f.GetResourceId().GetStorageId())
   407  		}
   408  	}
   409  	if resourceFilters != "" {
   410  		query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
   411  		params = append(params, resourceParams...)
   412  	}
   413  	if ownerFilters != "" {
   414  		query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
   415  		params = append(params, ownerParams...)
   416  	}
   417  	if creatorFilters != "" {
   418  		query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
   419  		params = append(params, creatorParams...)
   420  	}
   421  	if storageFilters != "" {
   422  		query = fmt.Sprintf("%s AND (%s)", query, storageFilters)
   423  		params = append(params, storageParams...)
   424  	}
   425  
   426  	rows, err := m.db.Query(query, params...)
   427  	if err != nil {
   428  		return nil, err
   429  	}
   430  	defer rows.Close()
   431  
   432  	var s DBShare
   433  	shares := []*link.PublicShare{}
   434  	for rows.Next() {
   435  		if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.FileSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions, &s.ItemStorage); err != nil {
   436  			continue
   437  		}
   438  		var cs3Share *link.PublicShare
   439  		if cs3Share, err = m.ConvertToCS3PublicShare(ctx, s); err != nil {
   440  			return nil, err
   441  		}
   442  		if publicshare.IsExpired(cs3Share) {
   443  			_ = m.cleanupExpiredShares()
   444  		} else {
   445  			if cs3Share.PasswordProtected && sign {
   446  				if err := publicshare.AddSignature(cs3Share, strings.TrimPrefix(s.ShareWith, "1|")); err != nil {
   447  					return nil, err
   448  				}
   449  			}
   450  			shares = append(shares, cs3Share)
   451  		}
   452  	}
   453  	if err = rows.Err(); err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	return shares, nil
   458  }
   459  
   460  func (m *mgr) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
   461  	uid := u.Username
   462  	query := "delete from oc_share where "
   463  	params := []interface{}{}
   464  
   465  	switch {
   466  	case ref.GetId() != nil && ref.GetId().OpaqueId != "":
   467  		query += "id=? AND (uid_owner=? or uid_initiator=?)"
   468  		params = append(params, ref.GetId().OpaqueId, uid, uid)
   469  	case ref.GetToken() != "":
   470  		query += "token=? AND (uid_owner=? or uid_initiator=?)"
   471  		params = append(params, ref.GetToken(), uid, uid)
   472  	default:
   473  		return errtypes.NotFound(ref.String())
   474  	}
   475  
   476  	stmt, err := m.db.Prepare(query)
   477  	if err != nil {
   478  		return err
   479  	}
   480  	res, err := stmt.Exec(params...)
   481  	if err != nil {
   482  		return err
   483  	}
   484  
   485  	rowCnt, err := res.RowsAffected()
   486  	if err != nil {
   487  		return err
   488  	}
   489  	if rowCnt == 0 {
   490  		return errtypes.NotFound(ref.String())
   491  	}
   492  	return nil
   493  }
   494  
   495  func (m *mgr) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
   496  	ps, err := m.getByToken(ctx, token)
   497  	if err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	if publicshare.IsExpired(&ps.PublicShare) {
   502  		if err := m.cleanupExpiredShares(); err != nil {
   503  			return nil, err
   504  		}
   505  		return nil, errtypes.NotFound("public share has expired")
   506  	}
   507  
   508  	if ps.PublicShare.PasswordProtected {
   509  		if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
   510  			return nil, errtypes.InvalidCredentials("access denied")
   511  		}
   512  	}
   513  
   514  	return &ps.PublicShare, nil
   515  }
   516  
   517  func (m *mgr) cleanupExpiredShares() error {
   518  	if !m.c.EnableExpiredSharesCleanup {
   519  		return nil
   520  	}
   521  
   522  	query := "DELETE FROM oc_share WHERE expiration IS NOT NULL AND expiration < ?"
   523  	params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
   524  
   525  	stmt, err := m.db.Prepare(query)
   526  	if err != nil {
   527  		return err
   528  	}
   529  	if _, err = stmt.Exec(params...); err != nil {
   530  		return err
   531  	}
   532  	return nil
   533  }