Use the opencloud reva from now on

This commit is contained in:
André Duffeck
2025-01-21 11:16:38 +01:00
parent 2c1afafb35
commit e8d35e1280
1007 changed files with 2988 additions and 27822 deletions
@@ -0,0 +1,620 @@
// Copyright 2018-2022 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 cs3
import (
"context"
"encoding/json"
"fmt"
"net/url"
"path"
"strconv"
"strings"
"sync"
"time"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/proto"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/indexer"
indexerErrors "github.com/opencloud-eu/reva/v2/pkg/storage/utils/indexer/errors"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/indexer/option"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
func init() {
registry.Register("cs3", NewDefault)
}
// Manager implements a publicshare manager using a cs3 storage backend
type Manager struct {
gatewayClient gateway.GatewayAPIClient
sync.RWMutex
storage metadata.Storage
indexer indexer.Indexer
passwordHashCost int
initialized bool
}
type config struct {
GatewayAddr string `mapstructure:"gateway_addr"`
ProviderAddr string `mapstructure:"provider_addr"`
ServiceUserID string `mapstructure:"service_user_id"`
ServiceUserIdp string `mapstructure:"service_user_idp"`
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
}
// NewDefault returns a new manager instance with default dependencies
func NewDefault(m map[string]interface{}) (publicshare.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error creating a new manager")
return nil, err
}
s, err := metadata.NewCS3Storage(c.GatewayAddr, c.ProviderAddr, c.ServiceUserID, c.ServiceUserIdp, c.MachineAuthAPIKey)
if err != nil {
return nil, err
}
indexer := indexer.CreateIndexer(s)
client, err := pool.GetGatewayServiceClient(c.GatewayAddr)
if err != nil {
return nil, err
}
return New(client, s, indexer, bcrypt.DefaultCost)
}
// New returns a new manager instance
func New(gatewayClient gateway.GatewayAPIClient, storage metadata.Storage, indexer indexer.Indexer, passwordHashCost int) (*Manager, error) {
return &Manager{
gatewayClient: gatewayClient,
storage: storage,
indexer: indexer,
passwordHashCost: passwordHashCost,
initialized: false,
}, nil
}
func (m *Manager) initialize() error {
if m.initialized {
return nil
}
m.Lock()
defer m.Unlock()
if m.initialized { // check if initialization happened while grabbing the lock
return nil
}
err := m.storage.Init(context.Background(), "public-share-manager-metadata")
if err != nil {
return err
}
if err := m.storage.MakeDirIfNotExist(context.Background(), "publicshares"); err != nil {
return err
}
err = m.indexer.AddIndex(&link.PublicShare{}, option.IndexByField("Id.OpaqueId"), "Token", "publicshares", "unique", nil, true)
if err != nil {
return err
}
err = m.indexer.AddIndex(&link.PublicShare{}, option.IndexByFunc{
Name: "Owner",
Func: indexOwnerFunc,
}, "Token", "publicshares", "non_unique", nil, true)
if err != nil {
return err
}
err = m.indexer.AddIndex(&link.PublicShare{}, option.IndexByFunc{
Name: "Creator",
Func: indexCreatorFunc,
}, "Token", "publicshares", "non_unique", nil, true)
if err != nil {
return err
}
err = m.indexer.AddIndex(&link.PublicShare{}, option.IndexByFunc{
Name: "ResourceId",
Func: indexResourceIDFunc,
}, "Token", "publicshares", "non_unique", nil, true)
if err != nil {
return err
}
m.initialized = true
return nil
}
// Dump exports public shares to channels (e.g. during migration)
func (m *Manager) Dump(ctx context.Context, shareChan chan<- *publicshare.WithPassword) error {
if err := m.initialize(); err != nil {
return err
}
pshares, err := m.storage.ListDir(ctx, "publicshares")
if err != nil {
return err
}
for _, v := range pshares {
var local publicshare.WithPassword
ps, err := m.getByToken(ctx, v.Name)
if err != nil {
return err
}
local.Password = ps.Password
proto.Merge(&local.PublicShare, &ps.PublicShare)
shareChan <- &local
}
return nil
}
// Load imports public shares and received shares from channels (e.g. during migration)
func (m *Manager) Load(ctx context.Context, shareChan <-chan *publicshare.WithPassword) error {
log := appctx.GetLogger(ctx)
if err := m.initialize(); err != nil {
return err
}
for ps := range shareChan {
if err := m.persist(context.Background(), ps); err != nil {
log.Error().Err(err).Interface("publicshare", ps).Msg("error loading public share")
}
}
return nil
}
// CreatePublicShare creates a new public share
func (m *Manager) CreatePublicShare(ctx context.Context, u *user.User, ri *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
id := &link.PublicShareId{
OpaqueId: utils.RandString(15),
}
tkn := utils.RandString(15)
now := time.Now().UnixNano()
displayName, quicklink := tkn, false
if ri.ArbitraryMetadata != nil {
metadataName, ok := ri.ArbitraryMetadata.Metadata["name"]
if ok {
displayName = metadataName
}
quicklink, _ = strconv.ParseBool(ri.ArbitraryMetadata.Metadata["quicklink"])
}
var passwordProtected bool
password := g.Password
if password != "" {
h, err := bcrypt.GenerateFromPassword([]byte(password), m.passwordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
password = string(h)
passwordProtected = true
}
createdAt := &typespb.Timestamp{
Seconds: uint64(now / int64(time.Second)),
Nanos: uint32(now % int64(time.Second)),
}
s := &publicshare.WithPassword{
PublicShare: link.PublicShare{
Id: id,
Owner: ri.GetOwner(),
Creator: u.Id,
ResourceId: ri.Id,
Token: tkn,
Permissions: g.Permissions,
Ctime: createdAt,
Mtime: createdAt,
PasswordProtected: passwordProtected,
Expiration: g.Expiration,
DisplayName: displayName,
Quicklink: quicklink,
},
Password: password,
}
err := m.persist(ctx, s)
if err != nil {
return nil, err
}
return &s.PublicShare, nil
}
// UpdatePublicShare updates an existing public share
func (m *Manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
ps, err := m.getWithPassword(ctx, req.Ref)
if err != nil {
return nil, err
}
switch req.Update.Type {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
ps.PublicShare.DisplayName = req.Update.DisplayName
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
ps.PublicShare.Permissions = req.Update.Grant.Permissions
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
ps.PublicShare.Expiration = req.Update.Grant.Expiration
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
if req.Update.Grant.Password == "" {
ps.Password = ""
ps.PublicShare.PasswordProtected = false
} else {
h, err := bcrypt.GenerateFromPassword([]byte(req.Update.Grant.Password), m.passwordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
ps.Password = string(h)
ps.PublicShare.PasswordProtected = true
}
default:
return nil, errtypes.BadRequest("no valid update type given")
}
err = m.persist(ctx, ps)
if err != nil {
return nil, err
}
return &ps.PublicShare, nil
}
// GetPublicShare returns an existing public share
func (m *Manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
ps, err := m.getWithPassword(ctx, ref)
if err != nil {
return nil, err
}
if ps.PublicShare.PasswordProtected && sign {
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
if err != nil {
return nil, err
}
}
return &ps.PublicShare, nil
}
func (m *Manager) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
switch {
case ref.GetToken() != "":
return m.getByToken(ctx, ref.GetToken())
case ref.GetId().GetOpaqueId() != "":
return m.getByID(ctx, ref.GetId().GetOpaqueId())
default:
return nil, errtypes.BadRequest("neither id nor token given")
}
}
func (m *Manager) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
tokens, err := m.indexer.FindBy(&link.PublicShare{},
indexer.NewField("Id.OpaqueId", id),
)
if err != nil {
return nil, err
}
if len(tokens) == 0 {
return nil, errtypes.NotFound("publicshare with the given id not found")
}
return m.getByToken(ctx, tokens[0])
}
func (m *Manager) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
fn := path.Join("publicshares", token)
data, err := m.storage.SimpleDownload(ctx, fn)
if err != nil {
return nil, err
}
ps := &publicshare.WithPassword{}
err = json.Unmarshal(data, ps)
if err != nil {
return nil, err
}
id := storagespace.UpdateLegacyResourceID(ps.PublicShare.ResourceId)
ps.PublicShare.ResourceId = id
return ps, nil
}
// ListPublicShares lists existing public shares matching the given filters
func (m *Manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
var rIDs []*provider.ResourceId
if len(filters) != 0 {
grouped := publicshare.GroupFiltersByType(filters)
for _, g := range grouped {
for _, f := range g {
if f.GetResourceId() != nil {
rIDs = append(rIDs, f.GetResourceId())
}
}
}
}
var (
createdShareTokens []string
err error
)
// in spaces, always use the resourceId
if len(rIDs) != 0 {
for _, rID := range rIDs {
shareTokens, err := m.indexer.FindBy(&link.PublicShare{},
indexer.NewField("ResourceId", resourceIDToIndex(rID)),
)
if err != nil {
return nil, err
}
createdShareTokens = append(createdShareTokens, shareTokens...)
}
} else {
// fallback for legacy use
createdShareTokens, err = m.indexer.FindBy(&link.PublicShare{},
indexer.NewField("Owner", userIDToIndex(u.Id)),
indexer.NewField("Creator", userIDToIndex(u.Id)),
)
if err != nil {
return nil, err
}
}
// We use shareMem as a temporary lookup store to check which shares were
// already added. This is to prevent duplicates.
shareMem := make(map[string]struct{})
result := []*link.PublicShare{}
for _, token := range createdShareTokens {
ps, err := m.getByToken(ctx, token)
if err != nil {
return nil, err
}
if !publicshare.MatchesFilters(&ps.PublicShare, filters) {
continue
}
if publicshare.IsExpired(&ps.PublicShare) {
ref := &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: ps.PublicShare.Id,
},
}
if err := m.RevokePublicShare(ctx, u, ref); err != nil {
log.Error().Err(err).
Str("public_share_token", ps.PublicShare.Token).
Str("public_share_id", ps.PublicShare.Id.OpaqueId).
Msg("failed to revoke expired public share")
}
continue
}
if ps.PublicShare.PasswordProtected && sign {
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
if err != nil {
return nil, err
}
}
result = append(result, &ps.PublicShare)
shareMem[ps.PublicShare.Token] = struct{}{}
}
// If a user requests to list shares which have not been created by them
// we have to explicitly fetch these shares and check if the user is
// allowed to list the shares.
// Only then can we add these shares to the result.
grouped := publicshare.GroupFiltersByType(filters)
idFilter, ok := grouped[link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID]
if !ok {
return result, nil
}
var tokens []string
if len(idFilter) > 0 {
idFilters := make([]indexer.Field, 0, len(idFilter))
for _, filter := range idFilter {
resourceID := filter.GetResourceId()
idFilters = append(idFilters, indexer.NewField("ResourceId", resourceIDToIndex(resourceID)))
}
tokens, err = m.indexer.FindBy(&link.PublicShare{}, idFilters...)
if err != nil {
return nil, err
}
}
// statMem is used as a local cache to prevent statting resources which
// already have been checked.
statMem := make(map[string]struct{})
for _, token := range tokens {
if _, handled := shareMem[token]; handled {
// We don't want to add a share multiple times when we added it
// already.
continue
}
s, err := m.getByToken(ctx, token)
if err != nil {
return nil, err
}
if _, checked := statMem[resourceIDToIndex(s.PublicShare.GetResourceId())]; !checked {
sReq := &provider.StatRequest{
Ref: &provider.Reference{ResourceId: s.PublicShare.GetResourceId()},
}
sRes, err := m.gatewayClient.Stat(ctx, sReq)
if err != nil {
continue
}
if sRes.Status.Code != rpc.Code_CODE_OK {
continue
}
if !sRes.Info.PermissionSet.ListGrants {
continue
}
statMem[resourceIDToIndex(s.PublicShare.GetResourceId())] = struct{}{}
}
if publicshare.MatchesFilters(&s.PublicShare, filters) {
result = append(result, &s.PublicShare)
shareMem[s.PublicShare.Token] = struct{}{}
}
}
return result, nil
}
// RevokePublicShare revokes an existing public share
func (m *Manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
if err := m.initialize(); err != nil {
return err
}
ps, err := m.GetPublicShare(ctx, u, ref, false)
if err != nil {
return err
}
err = m.storage.Delete(ctx, path.Join("publicshares", ps.Token))
if err != nil {
if _, ok := err.(errtypes.NotFound); !ok {
return err
}
}
return m.indexer.Delete(ps)
}
// GetPublicShareByToken gets an existing public share in an unauthenticated context using either a password or a signature
func (m *Manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
if err := m.initialize(); err != nil {
return nil, err
}
ps, err := m.getByToken(ctx, token)
if err != nil {
return nil, err
}
if publicshare.IsExpired(&ps.PublicShare) {
return nil, errtypes.NotFound("public share has expired")
}
if ps.PublicShare.PasswordProtected {
if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
return nil, errtypes.InvalidCredentials("access denied")
}
}
return &ps.PublicShare, nil
}
func indexOwnerFunc(v interface{}) (string, error) {
ps, ok := v.(*link.PublicShare)
if !ok {
return "", fmt.Errorf("given entity is not a public share")
}
return userIDToIndex(ps.Owner), nil
}
func indexCreatorFunc(v interface{}) (string, error) {
ps, ok := v.(*link.PublicShare)
if !ok {
return "", fmt.Errorf("given entity is not a public share")
}
return userIDToIndex(ps.Creator), nil
}
func indexResourceIDFunc(v interface{}) (string, error) {
ps, ok := v.(*link.PublicShare)
if !ok {
return "", fmt.Errorf("given entity is not a public share")
}
return resourceIDToIndex(ps.ResourceId), nil
}
func userIDToIndex(id *user.UserId) string {
return url.QueryEscape(id.Idp + ":" + id.OpaqueId)
}
func resourceIDToIndex(id *provider.ResourceId) string {
return strings.Join([]string{id.StorageId, id.OpaqueId}, "!")
}
func (m *Manager) persist(ctx context.Context, ps *publicshare.WithPassword) error {
data, err := json.Marshal(ps)
if err != nil {
return err
}
fn := path.Join("publicshares", ps.PublicShare.Token)
err = m.storage.SimpleUpload(ctx, fn, data)
if err != nil {
return err
}
_, err = m.indexer.Add(&ps.PublicShare)
if err != nil {
if _, ok := err.(*indexerErrors.AlreadyExistsErr); ok {
return nil
}
err = m.indexer.Delete(&ps.PublicShare)
if err != nil {
return err
}
_, err = m.indexer.Add(&ps.PublicShare)
return err
}
return nil
}
@@ -0,0 +1,727 @@
// 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 json
import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/proto"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence/cs3"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence/file"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence/memory"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
)
func init() {
registry.Register("json", NewFile)
registry.Register("jsoncs3", NewCS3)
registry.Register("jsonmemory", NewMemory)
}
// NewFile returns a new filesystem public shares manager.
func NewFile(c map[string]interface{}) (publicshare.Manager, error) {
conf := &fileConfig{}
if err := mapstructure.Decode(c, conf); err != nil {
return nil, err
}
conf.init()
if conf.File == "" {
conf.File = "/var/tmp/reva/publicshares"
}
p := file.New(conf.File)
return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p)
}
// NewMemory returns a new in-memory public shares manager.
func NewMemory(c map[string]interface{}) (publicshare.Manager, error) {
conf := &commonConfig{}
if err := mapstructure.Decode(c, conf); err != nil {
return nil, err
}
conf.init()
p := memory.New()
return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p)
}
// NewCS3 returns a new cs3 public shares manager.
func NewCS3(c map[string]interface{}) (publicshare.Manager, error) {
conf := &cs3Config{}
if err := mapstructure.Decode(c, conf); err != nil {
return nil, err
}
conf.init()
s, err := metadata.NewCS3Storage(conf.ProviderAddr, conf.ProviderAddr, conf.ServiceUserID, conf.ServiceUserIdp, conf.MachineAuthAPIKey)
if err != nil {
return nil, err
}
p := cs3.New(s)
return New(conf.GatewayAddr, conf.SharePasswordHashCost, conf.JanitorRunInterval, conf.EnableExpiredSharesCleanup, p)
}
// New returns a new public share manager instance
func New(gwAddr string, pwHashCost, janitorRunInterval int, enableCleanup bool, p persistence.Persistence) (publicshare.Manager, error) {
m := &manager{
gatewayAddr: gwAddr,
mutex: &sync.Mutex{},
passwordHashCost: pwHashCost,
janitorRunInterval: janitorRunInterval,
enableExpiredSharesCleanup: enableCleanup,
persistence: p,
}
go m.startJanitorRun()
return m, nil
}
type commonConfig struct {
GatewayAddr string `mapstructure:"gateway_addr"`
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
JanitorRunInterval int `mapstructure:"janitor_run_interval"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
}
type fileConfig struct {
commonConfig `mapstructure:",squash"`
File string `mapstructure:"file"`
}
type cs3Config struct {
commonConfig `mapstructure:",squash"`
ProviderAddr string `mapstructure:"provider_addr"`
ServiceUserID string `mapstructure:"service_user_id"`
ServiceUserIdp string `mapstructure:"service_user_idp"`
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
}
func (c *commonConfig) init() {
if c.SharePasswordHashCost == 0 {
c.SharePasswordHashCost = 11
}
if c.JanitorRunInterval == 0 {
c.JanitorRunInterval = 60
}
}
type manager struct {
gatewayAddr string
mutex *sync.Mutex
persistence persistence.Persistence
passwordHashCost int
janitorRunInterval int
enableExpiredSharesCleanup bool
}
func (m *manager) init() error {
return m.persistence.Init(context.Background())
}
func (m *manager) startJanitorRun() {
if !m.enableExpiredSharesCleanup {
return
}
ticker := time.NewTicker(time.Duration(m.janitorRunInterval) * time.Second)
work := make(chan os.Signal, 1)
signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
for {
select {
case <-work:
return
case <-ticker.C:
m.cleanupExpiredShares()
}
}
}
// Dump exports public shares to channels (e.g. during migration)
func (m *manager) Dump(ctx context.Context, shareChan chan<- *publicshare.WithPassword) error {
log := appctx.GetLogger(ctx)
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return err
}
db, err := m.persistence.Read(ctx)
if err != nil {
return err
}
for _, v := range db {
var local publicshare.WithPassword
if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local.PublicShare); err != nil {
log.Error().Err(err).Msg("error unmarshalling share")
}
local.Password = v.(map[string]interface{})["password"].(string)
shareChan <- &local
}
return nil
}
// Load imports public shares and received shares from channels (e.g. during migration)
func (m *manager) Load(ctx context.Context, shareChan <-chan *publicshare.WithPassword) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return err
}
db, err := m.persistence.Read(ctx)
if err != nil {
return err
}
for ps := range shareChan {
encShare, err := utils.MarshalProtoV1ToJSON(&ps.PublicShare)
if err != nil {
return err
}
db[ps.PublicShare.Id.GetOpaqueId()] = map[string]interface{}{
"share": string(encShare),
"password": ps.Password,
}
}
return m.persistence.Write(ctx, db)
}
// CreatePublicShare adds a new entry to manager.shares
func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
id := &link.PublicShareId{
OpaqueId: utils.RandString(15),
}
tkn := utils.RandString(15)
now := time.Now().UnixNano()
displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"]
if !ok {
displayName = tkn
}
quicklink, _ := strconv.ParseBool(rInfo.ArbitraryMetadata.Metadata["quicklink"])
var passwordProtected bool
password := g.Password
if len(password) > 0 {
h, err := bcrypt.GenerateFromPassword([]byte(password), m.passwordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
password = string(h)
passwordProtected = true
}
createdAt := &typespb.Timestamp{
Seconds: uint64(now / int64(time.Second)),
Nanos: uint32(now % int64(time.Second)),
}
s := &link.PublicShare{
Id: id,
Owner: rInfo.GetOwner(),
Creator: u.Id,
ResourceId: rInfo.Id,
Token: tkn,
Permissions: g.Permissions,
Ctime: createdAt,
Mtime: createdAt,
PasswordProtected: passwordProtected,
Expiration: g.Expiration,
DisplayName: displayName,
Quicklink: quicklink,
}
ps := &publicShare{
Password: password,
}
proto.Merge(&ps.PublicShare, s)
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return nil, err
}
encShare, err := utils.MarshalProtoV1ToJSON(&ps.PublicShare)
if err != nil {
return nil, err
}
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, err
}
if _, ok := db[s.Id.GetOpaqueId()]; !ok {
db[s.Id.GetOpaqueId()] = map[string]interface{}{
"share": string(encShare),
"password": ps.Password,
}
} else {
return nil, errors.New("key already exists")
}
err = m.persistence.Write(ctx, db)
if err != nil {
return nil, err
}
return s, nil
}
// UpdatePublicShare updates the public share
func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
log := appctx.GetLogger(ctx)
share, err := m.GetPublicShare(ctx, u, req.Ref, false)
if err != nil {
return nil, errors.New("ref does not exist")
}
now := time.Now().UnixNano()
var newPasswordEncoded string
passwordChanged := false
switch req.GetUpdate().GetType() {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
log.Debug().Str("json", "update display name").Msgf("from: `%v` to `%v`", share.DisplayName, req.Update.GetDisplayName())
share.DisplayName = req.Update.GetDisplayName()
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
old, _ := json.Marshal(share.Permissions)
new, _ := json.Marshal(req.Update.GetGrant().Permissions)
if req.GetUpdate().GetGrant().GetPassword() != "" {
passwordChanged = true
h, err := bcrypt.GenerateFromPassword([]byte(req.Update.GetGrant().Password), m.passwordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
newPasswordEncoded = string(h)
share.PasswordProtected = true
}
log.Debug().Str("json", "update grants").Msgf("from: `%v`\nto\n`%v`", old, new)
share.Permissions = req.Update.GetGrant().GetPermissions()
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
old, _ := json.Marshal(share.Expiration)
new, _ := json.Marshal(req.Update.GetGrant().Expiration)
log.Debug().Str("json", "update expiration").Msgf("from: `%v`\nto\n`%v`", old, new)
share.Expiration = req.Update.GetGrant().Expiration
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
passwordChanged = true
if req.Update.GetGrant().Password == "" {
share.PasswordProtected = false
newPasswordEncoded = ""
} else {
h, err := bcrypt.GenerateFromPassword([]byte(req.Update.GetGrant().Password), m.passwordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
newPasswordEncoded = string(h)
share.PasswordProtected = true
}
default:
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
}
share.Mtime = &typespb.Timestamp{
Seconds: uint64(now / int64(time.Second)),
Nanos: uint32(now % int64(time.Second)),
}
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return nil, err
}
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, err
}
encShare, err := utils.MarshalProtoV1ToJSON(share)
if err != nil {
return nil, err
}
data, ok := db[share.Id.OpaqueId].(map[string]interface{})
if !ok {
data = map[string]interface{}{}
}
if ok && passwordChanged {
data["password"] = newPasswordEncoded
}
data["share"] = string(encShare)
db[share.Id.OpaqueId] = data
err = m.persistence.Write(ctx, db)
if err != nil {
return nil, err
}
return share, nil
}
// GetPublicShare gets a public share either by ID or Token.
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return nil, err
}
if ref.GetToken() != "" {
ps, pw, err := m.getByToken(ctx, ref.GetToken())
if err != nil {
return nil, errtypes.NotFound("no shares found by token")
}
if ps.PasswordProtected && sign {
err := publicshare.AddSignature(ps, pw)
if err != nil {
return nil, err
}
}
return ps, nil
}
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, err
}
for _, v := range db {
d := v.(map[string]interface{})["share"]
passDB := v.(map[string]interface{})["password"].(string)
var ps link.PublicShare
if err := utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps); err != nil {
return nil, err
}
if ref.GetId().GetOpaqueId() == ps.Id.OpaqueId {
if publicshare.IsExpired(&ps) {
if err := m.revokeExpiredPublicShare(ctx, &ps); err != nil {
return nil, err
}
return nil, errtypes.NotFound("no shares found by id:" + ref.GetId().String())
}
if ps.PasswordProtected && sign {
err := publicshare.AddSignature(&ps, passDB)
if err != nil {
return nil, err
}
}
return &ps, nil
}
}
return nil, errtypes.NotFound("no shares found by id:" + ref.GetId().String())
}
// ListPublicShares retrieves all the shares on the manager that are valid.
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return nil, err
}
log := appctx.GetLogger(ctx)
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, err
}
client, err := pool.GetGatewayServiceClient(m.gatewayAddr)
if err != nil {
return nil, errors.Wrap(err, "failed to list shares")
}
cache := make(map[string]struct{})
shares := []*link.PublicShare{}
for _, v := range db {
var local publicShare
if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local.PublicShare); err != nil {
return nil, err
}
if publicshare.IsExpired(&local.PublicShare) {
if err := m.revokeExpiredPublicShare(ctx, &local.PublicShare); err != nil {
log.Error().Err(err).
Str("share_token", local.Token).
Msg("failed to revoke expired public share")
}
continue
}
if !publicshare.MatchesFilters(&local.PublicShare, filters) {
continue
}
key := strings.Join([]string{local.ResourceId.StorageId, local.ResourceId.OpaqueId}, "!")
if _, hit := cache[key]; !hit && !publicshare.IsCreatedByUser(&local.PublicShare, u) {
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: local.ResourceId}})
if err != nil {
log.Error().
Err(err).
Interface("resource_id", local.ResourceId).
Msg("ListShares: an error occurred during stat on the resource")
continue
}
if sRes.Status.Code != rpc.Code_CODE_OK {
if sRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
log.Debug().
Str("message", sRes.Status.Message).
Interface("status", sRes.Status).
Interface("resource_id", local.ResourceId).
Msg("ListShares: Resource not found")
continue
}
log.Error().
Str("message", sRes.Status.Message).
Interface("status", sRes.Status).
Interface("resource_id", local.ResourceId).
Msg("ListShares: could not stat resource")
continue
}
if !sRes.Info.PermissionSet.ListGrants {
// skip because the user doesn't have the permissions to list
// shares of this file.
continue
}
cache[key] = struct{}{}
}
if local.PublicShare.PasswordProtected && sign {
if err := publicshare.AddSignature(&local.PublicShare, local.Password); err != nil {
return nil, err
}
}
shares = append(shares, &local.PublicShare)
}
return shares, nil
}
func (m *manager) cleanupExpiredShares() {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return
}
db, _ := m.persistence.Read(context.Background())
for _, v := range db {
d := v.(map[string]interface{})["share"]
var ps link.PublicShare
_ = utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps)
if publicshare.IsExpired(&ps) {
_ = m.revokeExpiredPublicShare(context.Background(), &ps)
}
}
}
// revokeExpiredPublicShare doesn't have a lock inside, ensure a lock before call
func (m *manager) revokeExpiredPublicShare(ctx context.Context, s *link.PublicShare) error {
if !m.enableExpiredSharesCleanup {
return nil
}
err := m.revokePublicShare(ctx, &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: s.Id.OpaqueId,
},
},
})
if err != nil {
log.Err(err).Msg(fmt.Sprintf("publicShareJSONManager: error deleting public share with opaqueId: %s", s.Id.OpaqueId))
return err
}
return nil
}
// RevokePublicShare undocumented.
func (m *manager) RevokePublicShare(ctx context.Context, _ *user.User, ref *link.PublicShareReference) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return err
}
return m.revokePublicShare(ctx, ref)
}
// revokePublicShare doesn't have a lock inside, ensure a lock before call
func (m *manager) revokePublicShare(ctx context.Context, ref *link.PublicShareReference) error {
db, err := m.persistence.Read(ctx)
if err != nil {
return err
}
switch {
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
if _, ok := db[ref.GetId().OpaqueId]; ok {
delete(db, ref.GetId().OpaqueId)
} else {
return errors.New("reference does not exist")
}
case ref.GetToken() != "":
share, _, err := m.getByToken(ctx, ref.GetToken())
if err != nil {
return err
}
delete(db, share.Id.OpaqueId)
default:
return errors.New("reference does not exist")
}
return m.persistence.Write(ctx, db)
}
// getByToken doesn't have a lock inside, ensure a lock before call
func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicShare, string, error) {
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, "", err
}
for _, v := range db {
var local link.PublicShare
if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil {
return nil, "", err
}
if local.Token == token {
passDB := v.(map[string]interface{})["password"].(string)
return &local, passDB, nil
}
}
return nil, "", fmt.Errorf("share with token: `%v` not found", token)
}
// GetPublicShareByToken gets a public share by its opaque token.
func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.init(); err != nil {
return nil, err
}
db, err := m.persistence.Read(ctx)
if err != nil {
return nil, err
}
for _, v := range db {
passDB := v.(map[string]interface{})["password"].(string)
var local link.PublicShare
if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil {
return nil, err
}
if local.Token == token {
if publicshare.IsExpired(&local) {
if err := m.revokeExpiredPublicShare(ctx, &local); err != nil {
return nil, err
}
break
}
if local.PasswordProtected {
if publicshare.Authenticate(&local, passDB, auth) {
if sign {
err := publicshare.AddSignature(&local, passDB)
if err != nil {
return nil, err
}
}
return &local, nil
}
return nil, errtypes.InvalidCredentials("json: invalid password")
}
return &local, nil
}
}
return nil, errtypes.NotFound(fmt.Sprintf("share with token: `%v` not found", token))
}
type publicShare struct {
link.PublicShare
Password string `json:"password"`
}
@@ -0,0 +1,111 @@
// 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 cs3
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/metadata"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
type db struct {
mtime time.Time
publicShares persistence.PublicShares
}
type cs3 struct {
initialized bool
s metadata.Storage
db db
}
// New returns a new Cache instance
func New(s metadata.Storage) persistence.Persistence {
return &cs3{
s: s,
db: db{
publicShares: persistence.PublicShares{},
},
}
}
func (p *cs3) Init(ctx context.Context) error {
if p.initialized {
return nil
}
err := p.s.Init(ctx, "jsoncs3-public-share-manager-metadata")
if err != nil {
return err
}
p.initialized = true
return nil
}
func (p *cs3) Read(ctx context.Context) (persistence.PublicShares, error) {
if !p.initialized {
return nil, fmt.Errorf("not initialized")
}
info, err := p.s.Stat(ctx, "publicshares.json")
if err != nil {
if _, ok := err.(errtypes.NotFound); ok {
return p.db.publicShares, nil // Nothing to sync against
}
return nil, err
}
if utils.TSToTime(info.Mtime).After(p.db.mtime) {
readBytes, err := p.s.SimpleDownload(ctx, "publicshares.json")
if err != nil {
return nil, err
}
p.db.publicShares = persistence.PublicShares{}
if err := json.Unmarshal(readBytes, &p.db.publicShares); err != nil {
return nil, err
}
p.db.mtime = utils.TSToTime(info.Mtime)
}
return p.db.publicShares, nil
}
func (p *cs3) Write(ctx context.Context, db persistence.PublicShares) error {
if !p.initialized {
return fmt.Errorf("not initialized")
}
dbAsJSON, err := json.Marshal(db)
if err != nil {
return err
}
_, err = p.s.Upload(ctx, metadata.UploadRequest{
Content: dbAsJSON,
Path: "publicshares.json",
IfUnmodifiedSince: p.db.mtime,
})
return err
}
@@ -0,0 +1,111 @@
// 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 file
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence"
)
type file struct {
path string
initialized bool
lock *sync.RWMutex
}
// New returns a new Cache instance
func New(path string) persistence.Persistence {
return &file{
path: path,
lock: &sync.RWMutex{},
}
}
func (p *file) Init(_ context.Context) error {
if p.isInitialized() {
return nil
}
p.lock.Lock()
defer p.lock.Unlock()
if p.initialized {
return nil
}
// attempt to create the db file
var fi os.FileInfo
var err error
if fi, err = os.Stat(p.path); os.IsNotExist(err) {
folder := filepath.Dir(p.path)
if err := os.MkdirAll(folder, 0755); err != nil {
return err
}
if _, err := os.Create(p.path); err != nil {
return err
}
}
if fi == nil || fi.Size() == 0 {
err := os.WriteFile(p.path, []byte("{}"), 0644)
if err != nil {
return err
}
}
p.initialized = true
return nil
}
func (p *file) Read(_ context.Context) (persistence.PublicShares, error) {
if !p.isInitialized() {
return nil, fmt.Errorf("not initialized")
}
db := map[string]interface{}{}
readBytes, err := os.ReadFile(p.path)
if err != nil {
return nil, err
}
if err := json.Unmarshal(readBytes, &db); err != nil {
return nil, err
}
return db, nil
}
func (p *file) Write(_ context.Context, db persistence.PublicShares) error {
if !p.isInitialized() {
return fmt.Errorf("not initialized")
}
dbAsJSON, err := json.Marshal(db)
if err != nil {
return err
}
return os.WriteFile(p.path, dbAsJSON, 0644)
}
func (p *file) isInitialized() bool {
p.lock.RLock()
defer p.lock.RUnlock()
return p.initialized
}
@@ -0,0 +1,55 @@
// 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 memory
import (
"context"
"fmt"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json/persistence"
)
type memory struct {
db map[string]interface{}
}
// New returns a new Cache instance
func New() persistence.Persistence {
return &memory{
db: map[string]interface{}{},
}
}
func (p *memory) Init(_ context.Context) error {
return nil
}
func (p *memory) Read(_ context.Context) (persistence.PublicShares, error) {
if p.db == nil {
return nil, fmt.Errorf("not initialized")
}
return p.db, nil
}
func (p *memory) Write(_ context.Context, db persistence.PublicShares) error {
if p.db == nil {
return fmt.Errorf("not initialized")
}
p.db = db
return nil
}
@@ -0,0 +1,31 @@
// 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 persistence
import "context"
// PublicShares is a map indexing publicshares by their ids
type PublicShares map[string]interface{}
// Persistence defines the interface for the json publicshare manager persistence layers
type Persistence interface {
Init(context.Context) error
Read(context.Context) (PublicShares, error)
Write(context.Context, PublicShares) error
}
@@ -0,0 +1,28 @@
// 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 loader
import (
// Load core share manager drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/cs3"
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/json"
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/memory"
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/owncloudsql"
// Add your own here
)
@@ -0,0 +1,242 @@
// 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 memory
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"sync"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
func init() {
registry.Register("memory", New)
}
// New returns a new memory manager.
func New(c map[string]interface{}) (publicshare.Manager, error) {
return &manager{
shares: sync.Map{},
}, nil
}
type manager struct {
shares sync.Map
}
var (
passwordProtected bool
)
// CreatePublicShare adds a new entry to manager.shares
func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
id := &link.PublicShareId{
OpaqueId: randString(15),
}
tkn := randString(15)
now := uint64(time.Now().Unix())
displayName, ok := rInfo.ArbitraryMetadata.Metadata["name"]
if !ok {
displayName = tkn
}
if g.Password != "" {
passwordProtected = true
}
createdAt := &typespb.Timestamp{
Seconds: now,
Nanos: uint32(now % 1000000000),
}
modifiedAt := &typespb.Timestamp{
Seconds: now,
Nanos: uint32(now % 1000000000),
}
s := link.PublicShare{
Id: id,
Owner: rInfo.GetOwner(),
Creator: u.Id,
ResourceId: rInfo.Id,
Token: tkn,
Permissions: g.Permissions,
Ctime: createdAt,
Mtime: modifiedAt,
PasswordProtected: passwordProtected,
Expiration: g.Expiration,
DisplayName: displayName,
}
m.shares.Store(s.Token, &s)
return &s, nil
}
// UpdatePublicShare updates the expiration date, permissions and Mtime
func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
log := appctx.GetLogger(ctx)
share, err := m.GetPublicShare(ctx, u, req.Ref, false)
if err != nil {
return nil, errors.New("ref does not exist")
}
token := share.GetToken()
switch req.GetUpdate().GetType() {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
log.Debug().Str("memory", "update display name").Msgf("from: `%v` to `%v`", share.DisplayName, req.Update.GetDisplayName())
share.DisplayName = req.Update.GetDisplayName()
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
old, _ := json.Marshal(share.Permissions)
new, _ := json.Marshal(req.Update.GetGrant().Permissions)
log.Debug().Str("memory", "update grants").Msgf("from: `%v`\nto\n`%v`", old, new)
share.Permissions = req.Update.GetGrant().GetPermissions()
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
old, _ := json.Marshal(share.Expiration)
new, _ := json.Marshal(req.Update.GetGrant().Expiration)
log.Debug().Str("memory", "update expiration").Msgf("from: `%v`\nto\n`%v`", old, new)
share.Expiration = req.Update.GetGrant().Expiration
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
// TODO(refs) Do public shares need Grants? Struct is defined, just not used. Fill this once it's done.
fallthrough
default:
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
}
// share.Expiration = g.Expiration
share.Mtime = &typespb.Timestamp{
Seconds: uint64(time.Now().Unix()),
Nanos: uint32(time.Now().Unix() % 1000000000),
}
m.shares.Store(token, share)
return share, nil
}
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
// TODO(refs) return an error if the share is expired.
// Attempt to fetch public share by token
if ref.GetToken() != "" {
share, err = m.GetPublicShareByToken(ctx, ref.GetToken(), &link.PublicShareAuthentication{}, sign)
if err != nil {
return nil, errors.New("no shares found by token")
}
}
// Attempt to fetch public share by Id
if ref.GetId() != nil {
share, err = m.getPublicShareByTokenID(ctx, ref.GetId())
if err != nil {
return nil, errors.New("no shares found by id")
}
}
return
}
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
// TODO(refs) filter out expired shares
shares := []*link.PublicShare{}
m.shares.Range(func(k, v interface{}) bool {
s := v.(*link.PublicShare)
if len(filters) == 0 {
shares = append(shares, s)
} else {
for _, f := range filters {
if f.Type == link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID {
if utils.ResourceIDEqual(s.ResourceId, f.GetResourceId()) {
shares = append(shares, s)
}
}
}
}
return true
})
return shares, nil
}
func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
// check whether the reference exists
switch {
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
s, err := m.getPublicShareByTokenID(ctx, ref.GetId())
if err != nil {
return errors.New("reference does not exist")
}
m.shares.Delete(s.Token)
case ref.GetToken() != "":
if _, err := m.GetPublicShareByToken(ctx, ref.GetToken(), &link.PublicShareAuthentication{}, false); err != nil {
return errors.New("reference does not exist")
}
m.shares.Delete(ref.GetToken())
default:
return errors.New("reference does not exist")
}
return nil
}
func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
if ps, ok := m.shares.Load(token); ok {
return ps.(*link.PublicShare), nil
}
return nil, errtypes.NotFound("invalid token")
}
func randString(n int) string {
var l = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = l[rand.Intn(len(l))]
}
return string(b)
}
func (m *manager) getPublicShareByTokenID(ctx context.Context, targetID *link.PublicShareId) (*link.PublicShare, error) {
var found *link.PublicShare
m.shares.Range(func(k, v interface{}) bool {
id := v.(*link.PublicShare).GetId()
if targetID.String() == id.String() {
found = v.(*link.PublicShare)
}
return true
})
if found != nil {
return found, nil
}
return nil, errors.New("resource not found")
}
@@ -0,0 +1,226 @@
// 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 owncloudsql
import (
"context"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/jellydator/ttlcache/v2"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// DBShare stores information about user and public shares.
type DBShare struct {
ID string
UIDOwner string
UIDInitiator string
ItemStorage string
FileSource string
ItemType string // 'file' or 'folder'
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
RejectedBy string
State int
Parent 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
IDCache *ttlcache.Cache
NameCache *ttlcache.Cache
}
// NewGatewayUserConverter returns a instance of GatewayUserConverter
func NewGatewayUserConverter(gwAddr string) *GatewayUserConverter {
IDCache := ttlcache.NewCache()
_ = IDCache.SetTTL(30 * time.Second)
IDCache.SkipTTLExtensionOnHit(true)
NameCache := ttlcache.NewCache()
_ = NameCache.SetTTL(30 * time.Second)
NameCache.SkipTTLExtensionOnHit(true)
return &GatewayUserConverter{
gwAddr: gwAddr,
IDCache: IDCache,
NameCache: NameCache,
}
}
// UserIDToUserName converts a user ID to an username
func (c *GatewayUserConverter) UserIDToUserName(ctx context.Context, userid *userpb.UserId) (string, error) {
username, err := c.NameCache.Get(userid.String())
if err == nil {
return username.(string), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return "", err
}
getUserResponse, err := gwConn.GetUser(ctx, &userpb.GetUserRequest{
UserId: userid,
SkipFetchingUserGroups: true,
})
if err != nil {
return "", err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return "", status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.NameCache.Set(userid.String(), getUserResponse.User.Username)
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) {
id, err := c.IDCache.Get(username)
if err == nil {
return id.(*userpb.UserId), nil
}
gwConn, err := pool.GetGatewayServiceClient(c.gwAddr)
if err != nil {
return nil, err
}
getUserResponse, err := gwConn.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: username,
SkipFetchingUserGroups: true,
})
if err != nil {
return nil, err
}
if getUserResponse.Status.Code != rpc.Code_CODE_OK {
return nil, status.NewErrorFromCode(getUserResponse.Status.Code, "gateway")
}
_ = c.IDCache.Set(username, getUserResponse.User.Id)
return getUserResponse.User.Id, 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, true).OCSPermissions())
}
func intTosharePerm(p int) (*provider.ResourcePermissions, error) {
perms, err := conversions.NewPermissions(p)
if err != nil {
return nil, err
}
return conversions.RoleFromOCSPermissions(perms, nil).CS3ResourcePermissions(), nil
}
func formatUserID(u *userpb.UserId) string {
return u.OpaqueId
}
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
func (m *mgr) ConvertToCS3PublicShare(ctx context.Context, s DBShare) (*link.PublicShare, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
permissions, err := intTosharePerm(s.Permissions)
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
}
}
pwd := false
if s.ShareWith != "" {
pwd = true
}
var expires *typespb.Timestamp
if s.Expiration != "" {
t, err := time.Parse("2006-01-02 15:04:05", s.Expiration)
if err != nil {
t, err = time.Parse("2006-01-02 15:04:05-07:00", s.Expiration)
}
if err == nil {
expires = &typespb.Timestamp{
Seconds: uint64(t.Unix()),
}
}
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
SpaceId: s.ItemStorage,
OpaqueId: s.FileSource,
},
Permissions: &link.PublicSharePermissions{Permissions: permissions},
Owner: owner,
Creator: creator,
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
}, nil
}
@@ -0,0 +1,533 @@
// 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 owncloudsql implements a publiclink share manager backed by an existing ownCloud 10 database
//
// The SQL queries use `coalesce({column_identifier}, ”) as {column_identifier}` to read an emptystring
// instead of null values, which better fits the golang default values.
package owncloudsql
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"golang.org/x/crypto/bcrypt"
"google.golang.org/protobuf/proto"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
const (
publicShareType = 3
)
func init() {
registry.Register("owncloudsql", NewMysql)
}
// Config configures an owncloudsql publicshare manager
type Config struct {
GatewayAddr string `mapstructure:"gateway_addr"`
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"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
}
type mgr struct {
driver string
db *sql.DB
c Config
userConverter UserConverter
}
// NewMysql returns a new publicshare manager connection to a mysql database
func NewMysql(m map[string]interface{}) (publicshare.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(sharedconf.GetGatewaySVC(c.GatewayAddr))
return New("mysql", db, *c, userConverter)
}
// New returns a new Cache instance connecting to the given sql.DB
func New(driver string, db *sql.DB, c Config, userConverter UserConverter) (publicshare.Manager, error) {
if c.SharePasswordHashCost == 0 {
c.SharePasswordHashCost = bcrypt.DefaultCost
}
return &mgr{
driver: driver,
db: db,
c: c,
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) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) {
tkn := utils.RandString(15)
now := time.Now().Unix()
displayName := tkn
if rInfo.ArbitraryMetadata != nil && rInfo.ArbitraryMetadata.Metadata["name"] != "" {
displayName = rInfo.ArbitraryMetadata.Metadata["name"]
}
createdAt := &typespb.Timestamp{
Seconds: uint64(now),
}
creator := u.Username
owner, err := m.userConverter.UserIDToUserName(ctx, rInfo.Owner)
if err != nil {
return nil, err
}
permissions := sharePermToInt(g.Permissions.Permissions)
itemType := resourceTypeToItem(rInfo.Type)
itemSource := rInfo.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
}
columns := "share_type,uid_owner,uid_initiator,item_type,item_source,file_source,permissions,stime,token,share_name"
placeholders := "?,?,?,?,?,?,?,?,?,?"
params := []interface{}{publicShareType, owner, creator, itemType, itemSource, fileSource, permissions, now, tkn, displayName}
var passwordProtected bool
password := g.Password
if password != "" {
password, err = hashPassword(password, m.c.SharePasswordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
passwordProtected = true
columns += ",share_with"
placeholders += ",?"
params = append(params, password)
}
if g.Expiration != nil && g.Expiration.Seconds != 0 {
t := time.Unix(int64(g.Expiration.Seconds), 0)
columns += ",expiration"
placeholders += ",?"
params = append(params, t)
}
query := "INSERT INTO oc_share (" + columns + ") VALUES (" + placeholders + ")"
stmt, err := m.db.Prepare(query)
if err != nil {
return nil, err
}
result, err := stmt.Exec(params...)
if err != nil {
return nil, err
}
lastID, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: strconv.FormatInt(lastID, 10),
},
Owner: rInfo.GetOwner(),
Creator: u.Id,
ResourceId: rInfo.Id,
Token: tkn,
Permissions: g.Permissions,
Ctime: createdAt,
Mtime: createdAt,
PasswordProtected: passwordProtected,
Expiration: g.Expiration,
DisplayName: displayName,
}, nil
}
// owncloud 10 prefixes the hash with `1|`
func hashPassword(password string, cost int) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return "1|" + string(bytes), err
}
// UpdatePublicShare updates the expiration date, permissions and Mtime
func (m *mgr) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error) {
query := "update oc_share set "
paramsMap := map[string]interface{}{}
params := []interface{}{}
now := time.Now().Unix()
uid := u.Username
switch req.GetUpdate().GetType() {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
paramsMap["share_name"] = req.Update.GetDisplayName()
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
paramsMap["permissions"] = sharePermToInt(req.Update.GetGrant().GetPermissions().Permissions)
case link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION:
paramsMap["expiration"] = time.Unix(int64(req.Update.GetGrant().Expiration.Seconds), 0)
case link.UpdatePublicShareRequest_Update_TYPE_PASSWORD:
if req.Update.GetGrant().Password == "" {
paramsMap["share_with"] = ""
} else {
h, err := hashPassword(req.Update.GetGrant().Password, m.c.SharePasswordHashCost)
if err != nil {
return nil, errors.Wrap(err, "could not hash share password")
}
paramsMap["share_with"] = h
}
default:
return nil, fmt.Errorf("invalid update type: %v", req.GetUpdate().GetType())
}
for k, v := range paramsMap {
query += k + "=?"
params = append(params, v)
}
switch {
case req.Ref.GetId() != nil:
query += ",stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, now, req.Ref.GetId().OpaqueId, uid, uid)
case req.Ref.GetToken() != "":
query += ",stime=? where token=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, now, req.Ref.GetToken(), uid, uid)
default:
return nil, errtypes.NotFound(req.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.GetPublicShare(ctx, u, req.Ref, false)
}
func (m *mgr) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) {
ps, err := m.getWithPassword(ctx, ref)
if err != nil {
return nil, err
}
if publicshare.IsExpired(&ps.PublicShare) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound("public share has expired")
}
if ps.PublicShare.PasswordProtected && sign {
err = publicshare.AddSignature(&ps.PublicShare, ps.Password)
if err != nil {
return nil, err
}
}
return &ps.PublicShare, nil
}
func (m *mgr) getWithPassword(ctx context.Context, ref *link.PublicShareReference) (*publicshare.WithPassword, error) {
switch {
case ref.GetToken() != "":
return m.getByToken(ctx, ref.GetToken())
case ref.GetId().GetOpaqueId() != "":
return m.getByID(ctx, ref.GetId().GetOpaqueId())
default:
return nil, errtypes.BadRequest("neither id nor token given")
}
}
func (m *mgr) getByToken(ctx context.Context, token string) (*publicshare.WithPassword, error) {
s, err := getByToken(m.db, token)
if err != nil {
return nil, err
}
ps, err := m.ConvertToCS3PublicShare(ctx, s)
if err != nil {
return nil, err
}
ret := &publicshare.WithPassword{
Password: strings.TrimPrefix(s.ShareWith, "1|"),
}
proto.Merge(&ret.PublicShare, ps)
return ret, nil
}
func getByToken(db *sql.DB, token string) (DBShare, error) {
s := DBShare{Token: token}
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.id, s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE share_type=? AND token=?`
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 {
if err == sql.ErrNoRows {
return s, errtypes.NotFound(token)
}
return s, err
}
return s, nil
}
func (m *mgr) getByID(ctx context.Context, id string) (*publicshare.WithPassword, error) {
s := DBShare{ID: id}
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE share_type=? AND id=?`
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 {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id)
}
return nil, err
}
ps, err := m.ConvertToCS3PublicShare(ctx, s)
if err != nil {
return nil, err
}
ret := &publicshare.WithPassword{
Password: strings.TrimPrefix(s.ShareWith, "1|"),
}
proto.Merge(&ret.PublicShare, ps)
return ret, nil
}
func (m *mgr) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
uid := u.Username
// 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?
query := `SELECT
coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator,
coalesce(share_with, '') as share_with, coalesce(file_source, '') as file_source,
coalesce(item_type, '') as item_type, coalesce(token,'') as token,
coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name,
s.id, s.stime, s.permissions, fc.storage as storage
FROM oc_share s
LEFT JOIN oc_filecache fc ON fc.fileid = file_source
WHERE (uid_owner=? or uid_initiator=?)
AND (share_type=?)`
var resourceFilters, ownerFilters, creatorFilters, storageFilters string
var resourceParams, ownerParams, creatorParams, storageParams []interface{}
params := []interface{}{uid, uid, publicShareType}
for _, f := range filters {
switch f.Type {
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
if len(resourceFilters) != 0 {
resourceFilters += " OR "
}
resourceFilters += "item_source=?"
resourceParams = append(resourceParams, f.GetResourceId().GetOpaqueId())
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
if len(ownerFilters) != 0 {
ownerFilters += " OR "
}
ownerFilters += "(uid_owner=?)"
ownerParams = append(ownerParams, formatUserID(f.GetOwner()))
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
if len(creatorFilters) != 0 {
creatorFilters += " OR "
}
creatorFilters += "(uid_initiator=?)"
creatorParams = append(creatorParams, formatUserID(f.GetCreator()))
case publicshare.StorageIDFilterType:
if len(storageFilters) != 0 {
storageFilters += " OR "
}
storageFilters += "(storage=?)"
storageParams = append(storageParams, f.GetResourceId().GetStorageId())
}
}
if resourceFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, resourceFilters)
params = append(params, resourceParams...)
}
if ownerFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, ownerFilters)
params = append(params, ownerParams...)
}
if creatorFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, creatorFilters)
params = append(params, creatorParams...)
}
if storageFilters != "" {
query = fmt.Sprintf("%s AND (%s)", query, storageFilters)
params = append(params, storageParams...)
}
rows, err := m.db.Query(query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s DBShare
shares := []*link.PublicShare{}
for rows.Next() {
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 {
continue
}
var cs3Share *link.PublicShare
if cs3Share, err = m.ConvertToCS3PublicShare(ctx, s); err != nil {
return nil, err
}
if publicshare.IsExpired(cs3Share) {
_ = m.cleanupExpiredShares()
} else {
if cs3Share.PasswordProtected && sign {
if err := publicshare.AddSignature(cs3Share, strings.TrimPrefix(s.ShareWith, "1|")); err != nil {
return nil, err
}
}
shares = append(shares, cs3Share)
}
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
func (m *mgr) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
uid := u.Username
query := "delete from oc_share where "
params := []interface{}{}
switch {
case ref.GetId() != nil && ref.GetId().OpaqueId != "":
query += "id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetId().OpaqueId, uid, uid)
case ref.GetToken() != "":
query += "token=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetToken(), 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) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
ps, err := m.getByToken(ctx, token)
if err != nil {
return nil, err
}
if publicshare.IsExpired(&ps.PublicShare) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound("public share has expired")
}
if ps.PublicShare.PasswordProtected {
if !publicshare.Authenticate(&ps.PublicShare, ps.Password, auth) {
return nil, errtypes.InvalidCredentials("access denied")
}
}
return &ps.PublicShare, nil
}
func (m *mgr) cleanupExpiredShares() error {
if !m.c.EnableExpiredSharesCleanup {
return nil
}
query := "DELETE FROM oc_share WHERE expiration IS NOT NULL AND expiration < ?"
params := []interface{}{time.Now().Format("2006-01-02 03:04:05")}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
if _, err = stmt.Exec(params...); err != nil {
return err
}
return nil
}
@@ -0,0 +1,34 @@
// 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 registry
import "github.com/opencloud-eu/reva/v2/pkg/publicshare"
// NewFunc is the function that share managers
// should register at init time.
type NewFunc func(map[string]interface{}) (publicshare.Manager, error)
// NewFuncs is a map containing all the registered share managers.
var NewFuncs = map[string]NewFunc{}
// Register registers a new share manager new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewFunc) {
NewFuncs[name] = f
}
+220
View File
@@ -0,0 +1,220 @@
// 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 publicshare
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"time"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"golang.org/x/crypto/bcrypt"
)
const (
// StorageIDFilterType defines a new filter type for storage id.
// TODO: Remove this once the filter type is in the CS3 API.
StorageIDFilterType link.ListPublicSharesRequest_Filter_Type = 4
)
// Manager manipulates public shares.
type Manager interface {
CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error)
UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest) (*link.PublicShare, error)
GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error)
ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error)
RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error
GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error)
}
// WithPassword holds the relevant information for representing a public share
type WithPassword struct {
Password string `json:"password"`
PublicShare link.PublicShare
}
// DumpableManager defines a share manager which supports dumping its contents
type DumpableManager interface {
Dump(ctx context.Context, shareChan chan<- *WithPassword) error
}
// LoadableManager defines a share manager which supports loading contents from a dump
type LoadableManager interface {
Load(ctx context.Context, shareChan <-chan *WithPassword) error
}
// CreateSignature calculates a signature for a public share.
func CreateSignature(token, pw string, expiration time.Time) (string, error) {
h := sha256.New()
_, err := h.Write([]byte(pw))
if err != nil {
return "", err
}
key := make([]byte, 0, 32)
key = h.Sum(key)
mac := hmac.New(sha512.New512_256, key)
_, err = mac.Write([]byte(token + "|" + expiration.Format(time.RFC3339)))
if err != nil {
return "", err
}
sig := make([]byte, 0, 32)
sig = mac.Sum(sig)
return hex.EncodeToString(sig), nil
}
// AddSignature augments a public share with a signature.
// The signature has a validity of 30 minutes.
func AddSignature(share *link.PublicShare, pw string) error {
expiration := time.Now().Add(time.Minute * 30)
sig, err := CreateSignature(share.Token, pw, expiration)
if err != nil {
return err
}
share.Signature = &link.ShareSignature{
Signature: sig,
SignatureExpiration: &typesv1beta1.Timestamp{
Seconds: uint64(expiration.UnixNano() / 1000000000),
Nanos: uint32(expiration.UnixNano() % 1000000000),
},
}
return nil
}
// ResourceIDFilter is an abstraction for creating filter by resource id.
func ResourceIDFilter(id *provider.ResourceId) *link.ListPublicSharesRequest_Filter {
return &link.ListPublicSharesRequest_Filter{
Type: link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &link.ListPublicSharesRequest_Filter_ResourceId{
ResourceId: id,
},
}
}
// StorageIDFilter is an abstraction for creating filter by storage id.
func StorageIDFilter(id string) *link.ListPublicSharesRequest_Filter {
return &link.ListPublicSharesRequest_Filter{
Type: StorageIDFilterType,
Term: &link.ListPublicSharesRequest_Filter_ResourceId{
ResourceId: &provider.ResourceId{
StorageId: id,
},
},
}
}
// MatchesFilter tests if the share passes the filter.
func MatchesFilter(share *link.PublicShare, filter *link.ListPublicSharesRequest_Filter) bool {
switch filter.Type {
case link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID:
return utils.ResourceIDEqual(share.ResourceId, filter.GetResourceId())
case StorageIDFilterType:
return share.ResourceId.StorageId == filter.GetResourceId().GetStorageId()
default:
return false
}
}
// MatchesAnyFilter checks if the share passes at least one of the given filters.
func MatchesAnyFilter(share *link.PublicShare, filters []*link.ListPublicSharesRequest_Filter) bool {
for _, f := range filters {
if MatchesFilter(share, f) {
return true
}
}
return false
}
// MatchesFilters checks if the share passes the given filters.
// Filters of the same type form a disjuntion, a logical OR. Filters of separate type form a conjunction, a logical AND.
// Here is an example:
// (resource_id=1 OR resource_id=2) AND (grantee_type=USER OR grantee_type=GROUP)
func MatchesFilters(share *link.PublicShare, filters []*link.ListPublicSharesRequest_Filter) bool {
if len(filters) == 0 {
return true
}
grouped := GroupFiltersByType(filters)
for _, f := range grouped {
if !MatchesAnyFilter(share, f) {
return false
}
}
return true
}
// GroupFiltersByType groups the given filters and returns a map using the filter type as the key.
func GroupFiltersByType(filters []*link.ListPublicSharesRequest_Filter) map[link.ListPublicSharesRequest_Filter_Type][]*link.ListPublicSharesRequest_Filter {
grouped := make(map[link.ListPublicSharesRequest_Filter_Type][]*link.ListPublicSharesRequest_Filter)
for _, f := range filters {
grouped[f.Type] = append(grouped[f.Type], f)
}
return grouped
}
// IsExpired tests whether a public share is expired
func IsExpired(s *link.PublicShare) bool {
expiration := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos()))
return s.Expiration != nil && expiration.Before(time.Now())
}
// Authenticate checks the signature or password authentication for a public share
func Authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
switch {
case auth.GetPassword() != "":
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(auth.GetPassword())); err == nil {
return true
}
case auth.GetSignature() != nil:
sig := auth.GetSignature()
now := time.Now()
expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos()))
if now.After(expiration) {
return false
}
s, err := CreateSignature(share.Token, pw, expiration)
if err != nil {
return false
}
return sig.GetSignature() == s
}
return false
}
// IsCreatedByUser checks if a share was created by the user.
func IsCreatedByUser(share *link.PublicShare, user *user.User) bool {
return utils.UserEqual(user.Id, share.Owner) || utils.UserEqual(user.Id, share.Creator)
}
// IsWriteable checks if the grant for a publicshare allows writes or uploads.
func IsWriteable(perm *link.PublicSharePermissions) bool {
p := perm.GetPermissions()
return p != nil && (p.CreateContainer || p.Delete || p.InitiateFileUpload ||
p.Move || p.AddGrant || p.PurgeRecycle || p.RestoreFileVersion || p.RestoreRecycleItem)
}