Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -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.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,26 @@
// 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/json"
_ "github.com/opencloud-eu/reva/v2/pkg/publicshare/manager/memory"
// 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,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
}
@@ -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)
}