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 }