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
+139
View File
@@ -0,0 +1,139 @@
// 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 cbox
import (
"context"
"database/sql"
"fmt"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
)
func init() {
registry.Register("sql", New)
}
type config struct {
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"`
}
type mgr struct {
c *config
db *sql.DB
}
// New returns an instance of the cbox sql favorites manager.
func New(m map[string]interface{}) (favorite.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
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
}
return &mgr{
c: c,
db: db,
}, nil
}
func (m *mgr) ListFavorites(ctx context.Context, userID *user.UserId) ([]*provider.ResourceId, error) {
user := ctxpkg.ContextMustGetUser(ctx)
infos := []*provider.ResourceId{}
query := `SELECT fileid_prefix, fileid FROM cbox_metadata WHERE uid=? AND tag_key="fav"`
rows, err := m.db.Query(query, user.Id.OpaqueId)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var info provider.ResourceId
if err := rows.Scan(&info.SpaceId, &info.OpaqueId); err != nil {
return nil, err
}
infos = append(infos, &info)
}
if err = rows.Err(); err != nil {
return nil, err
}
return infos, nil
}
func (m *mgr) SetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
user := ctxpkg.ContextMustGetUser(ctx)
spaceID := resourceInfo.Id.SpaceId
// The primary key is just the ID in the table, it should ideally be (uid, fileid_prefix, fileid, tag_key)
// For the time being, just check if the favorite already exists. If it does, return early
var id int
query := `SELECT id FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`
if err := m.db.QueryRow(query, user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId).Scan(&id); err == nil {
// Favorite is already set, return
return nil
}
query = `INSERT INTO cbox_metadata SET item_type=?, uid=?, fileid_prefix=?, fileid=?, tag_key="fav"`
vals := []interface{}{utils.ResourceTypeToItemInt(resourceInfo.Type), user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
if _, err = stmt.Exec(vals...); err != nil {
return err
}
return nil
}
func (m *mgr) UnsetFavorite(ctx context.Context, userID *user.UserId, resourceInfo *provider.ResourceInfo) error {
user := ctxpkg.ContextMustGetUser(ctx)
spaceID := resourceInfo.Id.SpaceId
stmt, err := m.db.Prepare(`DELETE FROM cbox_metadata WHERE uid=? AND fileid_prefix=? AND fileid=? AND tag_key="fav"`)
if err != nil {
return err
}
res, err := stmt.Exec(user.Id.OpaqueId, spaceID, resourceInfo.Id.OpaqueId)
if err != nil {
return err
}
_, err = res.RowsAffected()
if err != nil {
return err
}
return nil
}
+221
View File
@@ -0,0 +1,221 @@
// 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 rest
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/gomodule/redigo/redis"
)
const (
groupPrefix = "group:"
idPrefix = "id:"
namePrefix = "name:"
gidPrefix = "gid:"
groupMembersPrefix = "members:"
groupInternalIDPrefix = "internal:"
)
func initRedisPool(address, username, password string) *redis.Pool {
return &redis.Pool{
MaxIdle: 50,
MaxActive: 1000,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
var c redis.Conn
var err error
switch {
case username != "":
c, err = redis.Dial("tcp", address,
redis.DialUsername(username),
redis.DialPassword(password),
)
case password != "":
c, err = redis.Dial("tcp", address,
redis.DialPassword(password),
)
default:
c, err = redis.Dial("tcp", address)
}
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
}
func (m *manager) setVal(key, val string, expiration int) error {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
args := []interface{}{key, val}
if expiration != -1 {
args = append(args, "EX", expiration)
}
if _, err := conn.Do("SET", args...); err != nil {
return err
}
return nil
}
return errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) getVal(key string) (string, error) {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
val, err := redis.String(conn.Do("GET", key))
if err != nil {
return "", err
}
return val, nil
}
return "", errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) fetchCachedInternalID(gid *grouppb.GroupId) (string, error) {
return m.getVal(groupPrefix + groupInternalIDPrefix + gid.OpaqueId)
}
func (m *manager) cacheInternalID(gid *grouppb.GroupId, internalID string) error {
return m.setVal(groupPrefix+groupInternalIDPrefix+gid.OpaqueId, internalID, -1)
}
func (m *manager) findCachedGroups(query string) ([]*grouppb.Group, error) {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
query = fmt.Sprintf("%s*%s*", groupPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
keys, err := redis.Strings(conn.Do("KEYS", query))
if err != nil {
return nil, err
}
var args []interface{}
for _, k := range keys {
args = append(args, k)
}
// Fetch the groups for all these keys
groupStrings, err := redis.Strings(conn.Do("MGET", args...))
if err != nil {
return nil, err
}
groupMap := make(map[string]*grouppb.Group)
for _, group := range groupStrings {
g := grouppb.Group{}
if err = json.Unmarshal([]byte(group), &g); err == nil {
groupMap[g.Id.OpaqueId] = &g
}
}
var groups []*grouppb.Group
for _, g := range groupMap {
groups = append(groups, g)
}
return groups, nil
}
return nil, errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) fetchCachedGroupDetails(gid *grouppb.GroupId) (*grouppb.Group, error) {
group, err := m.getVal(groupPrefix + idPrefix + gid.OpaqueId)
if err != nil {
return nil, err
}
g := grouppb.Group{}
if err = json.Unmarshal([]byte(group), &g); err != nil {
return nil, err
}
return &g, nil
}
func (m *manager) cacheGroupDetails(g *grouppb.Group) error {
encodedGroup, err := json.Marshal(&g)
if err != nil {
return err
}
if err = m.setVal(groupPrefix+idPrefix+strings.ToLower(g.Id.OpaqueId), string(encodedGroup), -1); err != nil {
return err
}
if g.GidNumber != 0 {
if err = m.setVal(groupPrefix+gidPrefix+strconv.FormatInt(g.GidNumber, 10), g.Id.OpaqueId, -1); err != nil {
return err
}
}
if g.DisplayName != "" {
if err = m.setVal(groupPrefix+namePrefix+g.Id.OpaqueId+"_"+strings.ToLower(g.DisplayName), g.Id.OpaqueId, -1); err != nil {
return err
}
}
return nil
}
func (m *manager) fetchCachedGroupByParam(field, claim string) (*grouppb.Group, error) {
group, err := m.getVal(groupPrefix + field + ":" + strings.ToLower(claim))
if err != nil {
return nil, err
}
g := grouppb.Group{}
if err = json.Unmarshal([]byte(group), &g); err != nil {
return nil, err
}
return &g, nil
}
func (m *manager) fetchCachedGroupMembers(gid *grouppb.GroupId) ([]*userpb.UserId, error) {
members, err := m.getVal(groupPrefix + groupMembersPrefix + strings.ToLower(gid.OpaqueId))
if err != nil {
return nil, err
}
u := []*userpb.UserId{}
if err = json.Unmarshal([]byte(members), &u); err != nil {
return nil, err
}
return u, nil
}
func (m *manager) cacheGroupMembers(gid *grouppb.GroupId, members []*userpb.UserId) error {
u, err := json.Marshal(&members)
if err != nil {
return err
}
return m.setVal(groupPrefix+groupMembersPrefix+strings.ToLower(gid.OpaqueId), string(u), m.conf.GroupMembersCacheExpiration*60)
}
+329
View File
@@ -0,0 +1,329 @@
// 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 rest
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/gomodule/redigo/redis"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
"github.com/opencloud-eu/reva/v2/pkg/group"
"github.com/opencloud-eu/reva/v2/pkg/group/manager/registry"
"github.com/rs/zerolog/log"
)
func init() {
registry.Register("rest", New)
}
type manager struct {
conf *config
redisPool *redis.Pool
apiTokenManager *utils.APITokenManager
}
type config struct {
// The address at which the redis server is running
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
// The username for connecting to the redis server
RedisUsername string `mapstructure:"redis_username" docs:""`
// The password for connecting to the redis server
RedisPassword string `mapstructure:"redis_password" docs:""`
// The time in minutes for which the members of a group would be cached
GroupMembersCacheExpiration int `mapstructure:"group_members_cache_expiration" docs:"5"`
// The OIDC Provider
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
// Base API Endpoint
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
// Client ID needed to authenticate
ClientID string `mapstructure:"client_id" docs:"-"`
// Client Secret
ClientSecret string `mapstructure:"client_secret" docs:"-"`
// Endpoint to generate token to access the API
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
// The target application for which token needs to be generated
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
// The time in seconds between bulk fetch of groups
GroupFetchInterval int `mapstructure:"group_fetch_interval" docs:"3600"`
}
func (c *config) init() {
if c.GroupMembersCacheExpiration == 0 {
c.GroupMembersCacheExpiration = 5
}
if c.RedisAddress == "" {
c.RedisAddress = ":6379"
}
if c.APIBaseURL == "" {
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
}
if c.TargetAPI == "" {
c.TargetAPI = "authorization-service-api"
}
if c.OIDCTokenEndpoint == "" {
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
}
if c.IDProvider == "" {
c.IDProvider = "http://cernbox.cern.ch"
}
if c.GroupFetchInterval == 0 {
c.GroupFetchInterval = 3600
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
// New returns a user manager implementation that makes calls to the GRAPPA API.
func New(m map[string]interface{}) (group.Manager, error) {
c, err := parseConfig(m)
if err != nil {
return nil, err
}
c.init()
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
mgr := &manager{
conf: c,
redisPool: redisPool,
apiTokenManager: apiTokenManager,
}
go mgr.fetchAllGroups()
return mgr, nil
}
func (m *manager) fetchAllGroups() {
_ = m.fetchAllGroupAccounts()
ticker := time.NewTicker(time.Duration(m.conf.GroupFetchInterval) * 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.fetchAllGroupAccounts()
}
}
}
func (m *manager) fetchAllGroupAccounts() error {
ctx := context.Background()
url := fmt.Sprintf("%s/api/v1.0/Group?field=groupIdentifier&field=displayName&field=gid", m.conf.APIBaseURL)
for url != "" {
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
if err != nil {
return err
}
responseData, ok := result["data"].([]interface{})
if !ok {
return errors.New("rest: error in type assertion")
}
for _, usr := range responseData {
groupData, ok := usr.(map[string]interface{})
if !ok {
continue
}
_, err = m.parseAndCacheGroup(ctx, groupData)
if err != nil {
continue
}
}
url = ""
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
if links, ok := pagination["links"].(map[string]interface{}); ok {
if next, ok := links["next"].(string); ok {
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
}
}
}
}
return nil
}
func (m *manager) parseAndCacheGroup(ctx context.Context, groupData map[string]interface{}) (*grouppb.Group, error) {
id, ok := groupData["groupIdentifier"].(string)
if !ok {
return nil, errors.New("rest: missing upn in user data")
}
name, _ := groupData["displayName"].(string)
groupID := &grouppb.GroupId{
OpaqueId: id,
Idp: m.conf.IDProvider,
}
gid, ok := groupData["gid"].(int64)
if !ok {
gid = 0
}
g := &grouppb.Group{
Id: groupID,
GroupName: id,
Mail: id + "@cern.ch",
DisplayName: name,
GidNumber: gid,
}
if err := m.cacheGroupDetails(g); err != nil {
log.Error().Err(err).Msg("rest: error caching group details")
}
if internalID, ok := groupData["id"].(string); ok {
if err := m.cacheInternalID(groupID, internalID); err != nil {
log.Error().Err(err).Msg("rest: error caching group details")
}
}
return g, nil
}
func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
g, err := m.fetchCachedGroupDetails(gid)
if err != nil {
return nil, err
}
if !skipFetchingMembers {
groupMembers, err := m.GetMembers(ctx, gid)
if err != nil {
return nil, err
}
g.Members = groupMembers
}
return g, nil
}
func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
if claim == "group_name" {
return m.GetGroup(ctx, &grouppb.GroupId{OpaqueId: value}, skipFetchingMembers)
}
g, err := m.fetchCachedGroupByParam(claim, value)
if err != nil {
return nil, err
}
if !skipFetchingMembers {
groupMembers, err := m.GetMembers(ctx, g.Id)
if err != nil {
return nil, err
}
g.Members = groupMembers
}
return g, nil
}
func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
// Look at namespaces filters. If the query starts with:
// "a" or none => get egroups
// other filters => get empty list
parts := strings.SplitN(query, ":", 2)
if len(parts) == 2 {
if parts[0] == "a" {
query = parts[1]
} else {
return []*grouppb.Group{}, nil
}
}
return m.findCachedGroups(query)
}
func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
users, err := m.fetchCachedGroupMembers(gid)
if err == nil {
return users, nil
}
internalID, err := m.fetchCachedInternalID(gid)
if err != nil {
return nil, err
}
url := fmt.Sprintf("%s/api/v1.0/Group/%s/memberidentities/precomputed", m.conf.APIBaseURL, internalID)
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
if err != nil {
return nil, err
}
userData := result["data"].([]interface{})
users = []*userpb.UserId{}
for _, u := range userData {
userInfo, ok := u.(map[string]interface{})
if !ok {
return nil, errors.New("rest: error in type assertion")
}
if id, ok := userInfo["upn"].(string); ok {
users = append(users, &userpb.UserId{OpaqueId: id, Idp: m.conf.IDProvider})
}
}
if err = m.cacheGroupMembers(gid, users); err != nil {
log := appctx.GetLogger(ctx)
log.Error().Err(err).Msg("rest: error caching group members")
}
return users, nil
}
func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
groupMemers, err := m.GetMembers(ctx, gid)
if err != nil {
return false, err
}
for _, u := range groupMemers {
if uid.OpaqueId == u.OpaqueId {
return true, nil
}
}
return false, nil
}
+31
View File
@@ -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 loader
import (
// Load cbox specific drivers.
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/favorite/sql"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/group/rest"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/preferences/sql"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/publicshare/sql"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/share/sql"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoshomewrapper"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/storage/eoswrapper"
_ "github.com/opencloud-eu/reva/v2/pkg/cbox/user/rest"
)
+100
View File
@@ -0,0 +1,100 @@
// 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 sql
import (
"context"
"database/sql"
"fmt"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/preferences"
"github.com/opencloud-eu/reva/v2/pkg/preferences/registry"
)
func init() {
registry.Register("sql", New)
}
type config struct {
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"`
}
type mgr struct {
c *config
db *sql.DB
}
// New returns an instance of the cbox sql preferences manager.
func New(m map[string]interface{}) (preferences.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
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
}
return &mgr{
c: c,
db: db,
}, nil
}
func (m *mgr) SetKey(ctx context.Context, key, namespace, value string) error {
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return errtypes.UserRequired("preferences: error getting user from ctx")
}
query := `INSERT INTO oc_preferences(userid, appid, configkey, configvalue) values(?, ?, ?, ?) ON DUPLICATE KEY UPDATE configvalue = ?`
params := []interface{}{user.Id.OpaqueId, namespace, key, value, value}
stmt, err := m.db.Prepare(query)
if err != nil {
return err
}
if _, err = stmt.Exec(params...); err != nil {
return err
}
return nil
}
func (m *mgr) GetKey(ctx context.Context, key, namespace string) (string, error) {
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
return "", errtypes.UserRequired("preferences: error getting user from ctx")
}
query := `SELECT configvalue FROM oc_preferences WHERE userid=? AND appid=? AND configkey=?`
var val string
if err := m.db.QueryRow(query, user.Id.OpaqueId, namespace, key).Scan(&val); err != nil {
if err == sql.ErrNoRows {
return "", errtypes.NotFound(namespace + ":" + key)
}
return "", err
}
return val, nil
}
+533
View File
@@ -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 sql
import (
"context"
"database/sql"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"golang.org/x/crypto/bcrypt"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
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"
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
"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/utils"
"github.com/pkg/errors"
)
const publicShareType = 3
func init() {
registry.Register("sql", New)
}
type config struct {
SharePasswordHashCost int `mapstructure:"password_hash_cost"`
JanitorRunInterval int `mapstructure:"janitor_run_interval"`
EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"`
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"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
type manager struct {
c *config
db *sql.DB
client gatewayv1beta1.GatewayAPIClient
}
func (c *config) init() {
if c.SharePasswordHashCost == 0 {
c.SharePasswordHashCost = 11
}
if c.JanitorRunInterval == 0 {
c.JanitorRunInterval = 3600
}
}
func (m *manager) startJanitorRun() {
if !m.c.EnableExpiredSharesCleanup {
return
}
ticker := time.NewTicker(time.Duration(m.c.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()
}
}
}
// New returns a new public share manager.
func New(m map[string]interface{}) (publicshare.Manager, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
c.init()
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
}
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return nil, err
}
mgr := manager{
c: c,
db: db,
client: gw,
}
go mgr.startJanitorRun()
return &mgr, nil
}
func (m *manager) 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, ok := rInfo.ArbitraryMetadata.Metadata["name"]
if !ok {
displayName = tkn
}
createdAt := &typespb.Timestamp{
Seconds: uint64(now),
}
creator := conversions.FormatUserID(u.Id)
owner := conversions.FormatUserID(rInfo.Owner)
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
itemType := conversions.ResourceTypeToItem(rInfo.Type)
prefix := rInfo.Id.SpaceId
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
}
query := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,token=?,share_name=?"
params := []interface{}{publicShareType, owner, creator, itemType, prefix, 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
query += ",share_with=?"
params = append(params, password)
}
if g.Expiration != nil && g.Expiration.Seconds != 0 {
t := time.Unix(int64(g.Expiration.Seconds), 0)
query += ",expiration=?"
params = append(params, t)
}
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
}
func (m *manager) 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 := conversions.FormatUserID(u.Id)
switch req.GetUpdate().GetType() {
case link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME:
paramsMap["share_name"] = req.Update.GetDisplayName()
case link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS:
paramsMap["permissions"] = conversions.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 *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) {
s := conversions.DBShare{Token: token}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND token=?"
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
if err == sql.ErrNoRows {
return nil, "", errtypes.NotFound(token)
}
return nil, "", err
}
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, "", err
}
return share, s.ShareWith, nil
}
func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) {
uid := conversions.FormatUserID(u.Id)
s := conversions.DBShare{ID: id.OpaqueId}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)"
if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions); err != nil {
if err == sql.ErrNoRows {
return nil, "", errtypes.NotFound(id.OpaqueId)
}
return nil, "", err
}
share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, "", err
}
return share, s.ShareWith, nil
}
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
var s *link.PublicShare
var pw string
var err error
switch {
case ref.GetId() != nil:
s, pw, err = m.getByID(ctx, ref.GetId(), u)
case ref.GetToken() != "":
s, pw, err = m.getByToken(ctx, ref.GetToken(), u)
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
if expired(s) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound(ref.String())
}
if s.PasswordProtected && sign {
if err := publicshare.AddSignature(s, pw); err != nil {
return nil, err
}
}
return s, nil
}
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, sign bool) ([]*link.PublicShare, error) {
uid := conversions.FormatUserID(u.Id)
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=?)"
var resourceFilters, ownerFilters, creatorFilters string
var resourceParams, ownerParams, creatorParams []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 += "(fileid_prefix=? AND item_source=?)"
resourceParams = append(resourceParams, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
case link.ListPublicSharesRequest_Filter_TYPE_OWNER:
if len(ownerFilters) != 0 {
ownerFilters += " OR "
}
ownerFilters += "(uid_owner=?)"
ownerParams = append(ownerParams, conversions.FormatUserID(f.GetOwner()))
case link.ListPublicSharesRequest_Filter_TYPE_CREATOR:
if len(creatorFilters) != 0 {
creatorFilters += " OR "
}
creatorFilters += "(uid_initiator=?)"
creatorParams = append(creatorParams, conversions.FormatUserID(f.GetCreator()))
}
}
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...)
}
rows, err := m.db.Query(query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s conversions.DBShare
shares := []*link.PublicShare{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Token, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
continue
}
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, err
}
if expired(cs3Share) {
_ = m.cleanupExpiredShares()
} else {
if cs3Share.PasswordProtected && sign {
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
return nil, err
}
}
shares = append(shares, cs3Share)
}
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error {
uid := conversions.FormatUserID(u.Id)
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 *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
s := conversions.DBShare{Token: token}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?"
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(token)
}
return nil, err
}
cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s)
if err != nil {
return nil, err
}
if s.ShareWith != "" {
if !authenticate(cs3Share, s.ShareWith, auth) {
// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
return nil, errtypes.InvalidCredentials(token)
}
if sign {
if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil {
return nil, err
}
}
}
if expired(cs3Share) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
}
return nil, errtypes.NotFound(token)
}
return cs3Share, nil
}
func (m *manager) cleanupExpiredShares() error {
if !m.c.EnableExpiredSharesCleanup {
return nil
}
query := "update oc_share set orphan = 1 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
}
func expired(s *link.PublicShare) bool {
if s.Expiration != nil {
if t := time.Unix(int64(s.Expiration.GetSeconds()), int64(s.Expiration.GetNanos())); t.Before(time.Now()) {
return true
}
}
return false
}
func hashPassword(password string, cost int) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
return "1|" + string(bytes), err
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password))
return err == nil
}
func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
switch {
case auth.GetPassword() != "":
return checkPasswordHash(auth.GetPassword(), pw)
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 := publicshare.CreateSignature(share.Token, pw, expiration)
if err != nil {
// TODO(labkode): pass context to call to log err.
// No we are blind
return false
}
return sig.GetSignature() == s
}
return false
}
+592
View File
@@ -0,0 +1,592 @@
// 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 sql
import (
"context"
"database/sql"
"fmt"
"path"
"strconv"
"strings"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/mitchellh/mapstructure"
conversions "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/share/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/genproto/protobuf/field_mask"
// Provides mysql drivers
_ "github.com/go-sql-driver/mysql"
)
const (
shareTypeUser = 0
shareTypeGroup = 1
)
func init() {
registry.Register("sql", New)
}
type config struct {
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"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
type mgr struct {
c *config
db *sql.DB
client gatewayv1beta1.GatewayAPIClient
}
// New returns a new share manager.
func New(m map[string]interface{}) (share.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
}
gw, err := pool.GetGatewayServiceClient(c.GatewaySvc)
if err != nil {
return nil, err
}
return &mgr{
c: c,
db: db,
client: gw,
}, 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) Share(ctx context.Context, md *provider.ResourceInfo, g *collaboration.ShareGrant) (*collaboration.Share, error) {
user := ctxpkg.ContextMustGetUser(ctx)
// do not allow share to myself or the owner if share is for a user
// TODO(labkode): should not this be caught already at the gw level?
if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER &&
(utils.UserEqual(g.Grantee.GetUserId(), user.Id) || utils.UserEqual(g.Grantee.GetUserId(), md.Owner)) {
return nil, errors.New("sql: owner/creator and grantee are the same")
}
// check if share already exists.
key := &collaboration.ShareKey{
Owner: md.Owner,
ResourceId: md.Id,
Grantee: g.Grantee,
}
_, err := m.getByKey(ctx, key)
// share already exists
if err == nil {
return nil, errtypes.AlreadyExists(key.String())
}
now := time.Now().Unix()
ts := &typespb.Timestamp{
Seconds: uint64(now),
}
shareType, shareWith := conversions.FormatGrantee(g.Grantee)
itemType := conversions.ResourceTypeToItem(md.Type)
targetPath := path.Join("/", path.Base(md.Path))
permissions := conversions.SharePermToInt(g.Permissions.Permissions)
prefix := md.Id.SpaceId
itemSource := md.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
}
stmtString := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,share_with=?,file_target=?"
stmtValues := []interface{}{shareType, conversions.FormatUserID(md.Owner), conversions.FormatUserID(user.Id), itemType, prefix, itemSource, fileSource, permissions, now, shareWith, targetPath}
stmt, err := m.db.Prepare(stmtString)
if err != nil {
return nil, err
}
result, err := stmt.Exec(stmtValues...)
if err != nil {
return nil, err
}
lastID, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: strconv.FormatInt(lastID, 10),
},
ResourceId: md.Id,
Permissions: g.Permissions,
Grantee: g.Grantee,
Owner: md.Owner,
Creator: user.Id,
Ctime: ts,
Mtime: ts,
}, nil
}
func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) {
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
s := conversions.DBShare{ID: id.OpaqueId}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND id=? AND (uid_owner=? or uid_initiator=?)"
if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.STime, &s.Permissions, &s.ShareType); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id.OpaqueId)
}
return nil, err
}
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) {
owner := conversions.FormatUserID(key.Owner)
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
s := conversions.DBShare{}
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
if err := m.db.QueryRow(query, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(key.String())
}
return nil, err
}
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) {
var s *collaboration.Share
var err error
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getByKey(ctx, ref.GetKey())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
return s, nil
}
func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error {
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
var query string
params := []interface{}{}
switch {
case ref.GetId() != nil:
query = "delete from oc_share where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, ref.GetId().OpaqueId, uid, uid)
case ref.GetKey() != nil:
key := ref.GetKey()
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
owner := conversions.FormatUserID(key.Owner)
query = "delete from oc_share where uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, 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) UpdateShare(ctx context.Context, ref *collaboration.ShareReference, p *collaboration.SharePermissions, updated *collaboration.Share, fieldMask *field_mask.FieldMask) (*collaboration.Share, error) {
permissions := conversions.SharePermToInt(p.Permissions)
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
var query string
params := []interface{}{}
switch {
case ref.GetId() != nil:
query = "update oc_share set permissions=?,stime=? where id=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, permissions, time.Now().Unix(), ref.GetId().OpaqueId, uid, uid)
case ref.GetKey() != nil:
key := ref.GetKey()
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
owner := conversions.FormatUserID(key.Owner)
query = "update oc_share set permissions=?,stime=? where (uid_owner=? or uid_initiator=?) AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)"
params = append(params, permissions, time.Now().Unix(), owner, owner, key.ResourceId.SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid)
default:
return nil, errtypes.NotFound(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.GetShare(ctx, ref)
}
func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) {
uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id)
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type,
id, stime, permissions, share_type
FROM oc_share
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)`
params := []interface{}{uid, uid, shareTypeUser, shareTypeGroup}
if len(filters) > 0 {
filterQuery, filterParams, err := translateFilters(filters)
if err != nil {
return nil, err
}
params = append(params, filterParams...)
if filterQuery != "" {
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
}
}
rows, err := m.db.Query(query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s conversions.DBShare
shares := []*collaboration.Share{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil {
continue
}
share, err := conversions.ConvertToCS3Share(ctx, m.client, s)
if err != nil {
continue
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
// we list the shares that are targeted to the user in context or to the user groups.
func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter, _ *userpb.UserId) ([]*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := conversions.FormatUserID(user.Id)
params := []interface{}{uid, uid, uid, uid}
for _, v := range user.Groups {
params = append(params, v)
}
query := `SELECT coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner != ? AND uid_initiator != ?)`
if len(user.Groups) > 0 {
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
} else {
query += " AND (share_with=? AND share_type = 0)"
}
filterQuery, filterParams, err := translateFilters(filters)
if err != nil {
return nil, err
}
params = append(params, filterParams...)
if filterQuery != "" {
query = fmt.Sprintf("%s AND (%s)", query, filterQuery)
}
rows, err := m.db.Query(query, params...)
if err != nil {
return nil, err
}
defer rows.Close()
var s conversions.DBShare
shares := []*collaboration.ReceivedShare{}
for rows.Next() {
if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
continue
}
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
continue
}
shares = append(shares, share)
}
if err = rows.Err(); err != nil {
return nil, err
}
return shares, nil
}
func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := conversions.FormatUserID(user.Id)
params := []interface{}{uid, id.OpaqueId, uid}
for _, v := range user.Groups {
params = append(params, v)
}
s := conversions.DBShare{ID: id.OpaqueId}
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
stime, permissions, share_type, coalesce(tr.state, 0) as state
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
WHERE (orphan = 0 or orphan IS NULL) AND ts.id=?`
if len(user.Groups) > 0 {
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
} else {
query += " AND (share_with=? AND share_type = 0)"
}
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id.OpaqueId)
}
return nil, err
}
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
uid := conversions.FormatUserID(user.Id)
shareType, shareWith := conversions.FormatGrantee(key.Grantee)
params := []interface{}{uid, conversions.FormatUserID(key.Owner), key.GetResourceId().SpaceId, key.ResourceId.OpaqueId, shareType, shareWith, shareWith}
for _, v := range user.Groups {
params = append(params, v)
}
s := conversions.DBShare{}
query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with,
coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(file_target, '') as file_target,
ts.id, stime, permissions, share_type, coalesce(tr.state, 0) as state
FROM oc_share ts LEFT JOIN oc_share_status tr ON (ts.id = tr.id AND tr.recipient = ?)
WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=?`
if len(user.Groups) > 0 {
query += " AND ((share_with=? AND share_type = 0) OR (share_type = 1 AND share_with in (?" + strings.Repeat(",?", len(user.Groups)-1) + ")))"
} else {
query += " AND (share_with=? AND share_type = 0)"
}
if err := m.db.QueryRow(query, params...).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.FileTarget, &s.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(key.String())
}
return nil, err
}
share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s)
if err != nil {
return nil, err
}
return share, nil
}
func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) {
var s *collaboration.ReceivedShare
var err error
switch {
case ref.GetId() != nil:
s, err = m.getReceivedByID(ctx, ref.GetId())
case ref.GetKey() != nil:
s, err = m.getReceivedByKey(ctx, ref.GetKey())
default:
err = errtypes.NotFound(ref.String())
}
if err != nil {
return nil, err
}
return s, nil
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask, _ *userpb.UserId) (*collaboration.ReceivedShare, error) {
user := ctxpkg.ContextMustGetUser(ctx)
rs, err := m.GetReceivedShare(ctx, &collaboration.ShareReference{Spec: &collaboration.ShareReference_Id{Id: share.Share.Id}})
if err != nil {
return nil, err
}
for i := range fieldMask.Paths {
switch fieldMask.Paths[i] {
case "state":
rs.State = share.State
case "mount_point":
rs.MountPoint = share.MountPoint
default:
return nil, errtypes.NotSupported("updating " + fieldMask.Paths[i] + " is not supported")
}
}
state := 0
switch rs.GetState() {
case collaboration.ShareState_SHARE_STATE_REJECTED:
state = -1
case collaboration.ShareState_SHARE_STATE_ACCEPTED:
state = 1
}
params := []interface{}{rs.Share.Id.OpaqueId, conversions.FormatUserID(user.Id), state, state}
query := "insert into oc_share_status(id, recipient, state) values(?, ?, ?) ON DUPLICATE KEY UPDATE state = ?"
stmt, err := m.db.Prepare(query)
if err != nil {
return nil, err
}
_, err = stmt.Exec(params...)
if err != nil {
return nil, err
}
return rs, nil
}
func granteeTypeToShareType(granteeType provider.GranteeType) int {
switch granteeType {
case provider.GranteeType_GRANTEE_TYPE_USER:
return shareTypeUser
case provider.GranteeType_GRANTEE_TYPE_GROUP:
return shareTypeGroup
}
return -1
}
// translateFilters translates the filters to sql queries
func translateFilters(filters []*collaboration.Filter) (string, []interface{}, error) {
var (
filterQuery string
params []interface{}
)
groupedFilters := share.GroupFiltersByType(filters)
// If multiple filters of the same type are passed to this function, they need to be combined with the `OR` operator.
// That is why the filters got grouped by type.
// For every given filter type, iterate over the filters and if there are more than one combine them.
// Combine the different filter types using `AND`
var filterCounter = 0
for filterType, filters := range groupedFilters {
switch filterType {
case collaboration.Filter_TYPE_RESOURCE_ID:
filterQuery += "("
for i, f := range filters {
filterQuery += "(fileid_prefix =? AND item_source=?)"
params = append(params, f.GetResourceId().SpaceId, f.GetResourceId().OpaqueId)
if i != len(filters)-1 {
filterQuery += " OR "
}
}
filterQuery += ")"
case collaboration.Filter_TYPE_GRANTEE_TYPE:
filterQuery += "("
for i, f := range filters {
filterQuery += "share_type=?"
params = append(params, granteeTypeToShareType(f.GetGranteeType()))
if i != len(filters)-1 {
filterQuery += " OR "
}
}
filterQuery += ")"
case collaboration.Filter_TYPE_EXCLUDE_DENIALS:
// TODO this may change once the mapping of permission to share types is completed (cf. pkg/cbox/utils/conversions.go)
filterQuery += "(permissions > 0)"
default:
return "", nil, fmt.Errorf("filter type is not supported")
}
if filterCounter != len(groupedFilters)-1 {
filterQuery += " AND "
}
filterCounter++
}
return filterQuery, params, nil
}
@@ -0,0 +1,129 @@
// 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 eoshomewrapper
import (
"bytes"
"context"
"text/template"
"github.com/Masterminds/sprig"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
func init() {
registry.Register("eoshomewrapper", New)
}
type wrapper struct {
storage.FS
mountIDTemplate *template.Template
}
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
c := &eosfs.Config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, "", err
}
// default to version invariance if not configured
if _, ok := m["version_invariant"]; !ok {
c.VersionInvariant = true
}
t, ok := m["mount_id_template"].(string)
if !ok || t == "" {
t = "eoshome-{{substr 0 1 .Username}}"
}
return c, t, nil
}
// New returns an implementation of the storage.FS interface that forms a wrapper
// around separate connections to EOS.
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
c, t, err := parseConfig(m)
if err != nil {
return nil, err
}
c.EnableHome = true
eos, err := eosfs.NewEOSFS(c)
if err != nil {
return nil, err
}
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
if err != nil {
return nil, err
}
return &wrapper{FS: eos, mountIDTemplate: mountIDTemplate}, nil
}
// We need to override the two methods, GetMD and ListFolder to fill the
// StorageId in the ResourceInfo objects.
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
if err != nil {
return nil, err
}
// We need to extract the mount ID based on the mapping template.
//
// Take the first letter of the username of the logged-in user, as the home
// storage provider restricts requests only to the home namespace.
res.Id.StorageId = w.getMountID(ctx, res)
return res, nil
}
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
if err != nil {
return nil, err
}
for _, r := range res {
r.Id.StorageId = w.getMountID(ctx, r)
}
return res, nil
}
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
}
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
u := ctxpkg.ContextMustGetUser(ctx)
b := bytes.Buffer{}
if err := w.mountIDTemplate.Execute(&b, u); err != nil {
return ""
}
return b.String()
}
@@ -0,0 +1,296 @@
// 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 eoswrapper
import (
"bytes"
"context"
"io"
"path"
"strings"
"text/template"
"github.com/Masterminds/sprig"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/eosfs"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
func init() {
registry.Register("eoswrapper", New)
}
const (
eosProjectsNamespace = "/eos/project"
// We can use a regex for these, but that might have inferior performance
projectSpaceGroupsPrefix = "cernbox-project-"
projectSpaceAdminGroupsSuffix = "-admins"
)
type wrapper struct {
storage.FS
conf *eosfs.Config
mountIDTemplate *template.Template
}
func parseConfig(m map[string]interface{}) (*eosfs.Config, string, error) {
c := &eosfs.Config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, "", err
}
// default to version invariance if not configured
if _, ok := m["version_invariant"]; !ok {
c.VersionInvariant = true
}
// allow recycle operations for project spaces
if !c.EnableHome && strings.HasPrefix(c.Namespace, eosProjectsNamespace) {
c.AllowPathRecycleOperations = true
c.ImpersonateOwnerforRevisions = true
}
t, ok := m["mount_id_template"].(string)
if !ok || t == "" {
t = "eoshome-{{ trimAll \"/\" .Path | substr 0 1 }}"
}
return c, t, nil
}
// New returns an implementation of the storage.FS interface that forms a wrapper
// around separate connections to EOS.
func New(m map[string]interface{}, _ events.Stream, _ *zerolog.Logger) (storage.FS, error) {
c, t, err := parseConfig(m)
if err != nil {
return nil, err
}
eos, err := eosfs.NewEOSFS(c)
if err != nil {
return nil, err
}
mountIDTemplate, err := template.New("mountID").Funcs(sprig.TxtFuncMap()).Parse(t)
if err != nil {
return nil, err
}
return &wrapper{FS: eos, conf: c, mountIDTemplate: mountIDTemplate}, nil
}
// We need to override the methods, GetMD, GetPathByID and ListFolder to fill the
// StorageId in the ResourceInfo objects.
func (w *wrapper) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string, fieldMask []string) (*provider.ResourceInfo, error) {
res, err := w.FS.GetMD(ctx, ref, mdKeys, fieldMask)
if err != nil {
return nil, err
}
// We need to extract the mount ID based on the mapping template.
//
// Take the first letter of the resource path after the namespace has been removed.
// If it's empty, leave it empty to be filled by storageprovider.
res.Id.StorageId = w.getMountID(ctx, res)
if err = w.setProjectSharingPermissions(ctx, res); err != nil {
return nil, err
}
// If the request contains a relative reference, we also need to return the base path instead of the full one
if utils.IsRelativeReference(ref) {
res.Path = path.Base(res.Path)
}
return res, nil
}
func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys, fieldMask []string) ([]*provider.ResourceInfo, error) {
res, err := w.FS.ListFolder(ctx, ref, mdKeys, fieldMask)
if err != nil {
return nil, err
}
for _, r := range res {
r.Id.StorageId = w.getMountID(ctx, r)
// If the request contains a relative reference, we also need to return the base path instead of the full one
if utils.IsRelativeReference(ref) {
r.Path = path.Base(r.Path)
}
if err = w.setProjectSharingPermissions(ctx, r); err != nil {
continue
}
}
return res, nil
}
func (w *wrapper) ListRecycle(ctx context.Context, ref *provider.Reference, key, relativePath string) ([]*provider.RecycleItem, error) {
res, err := w.FS.ListRecycle(ctx, ref, key, relativePath)
if err != nil {
return nil, err
}
// If the request contains a relative reference, we also need to return the base path instead of the full one
if utils.IsRelativeReference(ref) {
for _, info := range res {
info.Ref.Path = path.Base(info.Ref.Path)
}
}
return res, nil
}
func (w *wrapper) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter, unrestricted bool) ([]*provider.StorageSpace, error) {
res, err := w.FS.ListStorageSpaces(ctx, filter, unrestricted)
if err != nil {
return nil, err
}
for _, r := range res {
if mountID, _, _, _ := storagespace.SplitID(r.Id.OpaqueId); mountID == "" {
mountID = w.getMountID(ctx, &provider.ResourceInfo{Path: r.Name})
r.Root.StorageId = mountID
}
}
return res, nil
}
func (w *wrapper) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) {
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
return nil, err
}
return w.FS.ListRevisions(ctx, ref)
}
func (w *wrapper) DownloadRevision(ctx context.Context, ref *provider.Reference, revisionKey string, openReaderfunc func(*provider.ResourceInfo) bool) (*provider.ResourceInfo, io.ReadCloser, error) {
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
return nil, nil, err
}
return w.FS.DownloadRevision(ctx, ref, revisionKey, openReaderfunc)
}
func (w *wrapper) RestoreRevision(ctx context.Context, ref *provider.Reference, revisionKey string) error {
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
return err
}
return w.FS.RestoreRevision(ctx, ref, revisionKey)
}
func (w *wrapper) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error {
// This is only allowed for project space admins
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
return err
}
return w.FS.DenyGrant(ctx, ref, g)
}
return errtypes.NotSupported("eos: deny grant is only enabled for project spaces")
}
func (w *wrapper) getMountID(ctx context.Context, r *provider.ResourceInfo) string {
if r == nil {
return ""
}
r.Path = strings.TrimPrefix(r.Path, w.conf.MountPath)
b := bytes.Buffer{}
if err := w.mountIDTemplate.Execute(&b, r); err != nil {
return ""
}
r.Path = path.Join(w.conf.MountPath, r.Path)
return b.String()
}
func (w *wrapper) setProjectSharingPermissions(ctx context.Context, r *provider.ResourceInfo) error {
// Check if this storage provider corresponds to a project spaces instance
if strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
parts := strings.SplitN(r.Path, "/", 4)
if len(parts) != 4 && len(parts) != 3 {
// The request might be for / or /$letter
// Nothing to do in that case
return nil
}
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
user := ctxpkg.ContextMustGetUser(ctx)
for _, g := range user.Groups {
if g == adminGroup {
r.PermissionSet.AddGrant = true
r.PermissionSet.RemoveGrant = true
r.PermissionSet.UpdateGrant = true
r.PermissionSet.ListGrants = true
r.PermissionSet.GetQuota = true
r.PermissionSet.DenyGrant = true
return nil
}
}
}
return nil
}
func (w *wrapper) userIsProjectAdmin(ctx context.Context, ref *provider.Reference) error {
// Check if this storage provider corresponds to a project spaces instance
if !strings.HasPrefix(w.conf.Namespace, eosProjectsNamespace) {
return nil
}
res, err := w.FS.GetMD(ctx, ref, nil, nil)
if err != nil {
return err
}
// Extract project name from the path resembling /c/cernbox or /c/cernbox/minutes/..
parts := strings.SplitN(res.Path, "/", 4)
if len(parts) != 4 && len(parts) != 3 {
// The request might be for / or /$letter
// Nothing to do in that case
return nil
}
adminGroup := projectSpaceGroupsPrefix + parts[2] + projectSpaceAdminGroupsSuffix
user := ctxpkg.ContextMustGetUser(ctx)
for _, g := range user.Groups {
if g == adminGroup {
return nil
}
}
return errtypes.PermissionDenied("eosfs: project spaces revisions can only be accessed by admins")
}
+214
View File
@@ -0,0 +1,214 @@
// 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 rest
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/gomodule/redigo/redis"
)
const (
userPrefix = "user:"
usernamePrefix = "username:"
userIDPrefix = "userid:"
namePrefix = "name:"
mailPrefix = "mail:"
uidPrefix = "uid:"
userGroupsPrefix = "groups:"
)
func initRedisPool(address, username, password string) *redis.Pool {
return &redis.Pool{
MaxIdle: 50,
MaxActive: 1000,
IdleTimeout: 240 * time.Second,
Dial: func() (redis.Conn, error) {
var opts []redis.DialOption
if username != "" {
opts = append(opts, redis.DialUsername(username))
}
if password != "" {
opts = append(opts, redis.DialPassword(password))
}
c, err := redis.Dial("tcp", address, opts...)
if err != nil {
return nil, err
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
}
func (m *manager) setVal(key, val string, expiration int) error {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
args := []interface{}{key, val}
if expiration != -1 {
args = append(args, "EX", expiration)
}
if _, err := conn.Do("SET", args...); err != nil {
return err
}
return nil
}
return errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) getVal(key string) (string, error) {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
val, err := redis.String(conn.Do("GET", key))
if err != nil {
return "", err
}
return val, nil
}
return "", errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) findCachedUsers(query string) ([]*userpb.User, error) {
conn := m.redisPool.Get()
defer conn.Close()
if conn != nil {
query = fmt.Sprintf("%s*%s*", userPrefix, strings.ReplaceAll(strings.ToLower(query), " ", "_"))
keys, err := redis.Strings(conn.Do("KEYS", query))
if err != nil {
return nil, err
}
var args []interface{}
for _, k := range keys {
args = append(args, k)
}
// Fetch the users for all these keys
userStrings, err := redis.Strings(conn.Do("MGET", args...))
if err != nil {
return nil, err
}
userMap := make(map[string]*userpb.User)
for _, user := range userStrings {
u := userpb.User{}
if err = json.Unmarshal([]byte(user), &u); err == nil {
userMap[u.Id.OpaqueId] = &u
}
}
var users []*userpb.User
for _, u := range userMap {
users = append(users, u)
}
return users, nil
}
return nil, errors.New("rest: unable to get connection from redis pool")
}
func (m *manager) fetchCachedUserDetails(uid *userpb.UserId) (*userpb.User, error) {
user, err := m.getVal(userPrefix + usernamePrefix + strings.ToLower(uid.OpaqueId))
if err != nil {
return nil, err
}
u := userpb.User{}
if err = json.Unmarshal([]byte(user), &u); err != nil {
return nil, err
}
return &u, nil
}
func (m *manager) cacheUserDetails(u *userpb.User) error {
encodedUser, err := json.Marshal(&u)
if err != nil {
return err
}
if err = m.setVal(userPrefix+usernamePrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
return err
}
if err = m.setVal(userPrefix+userIDPrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), -1); err != nil {
return err
}
if u.Mail != "" {
if err = m.setVal(userPrefix+mailPrefix+strings.ToLower(u.Mail), string(encodedUser), -1); err != nil {
return err
}
}
if u.DisplayName != "" {
if err = m.setVal(userPrefix+namePrefix+u.Id.OpaqueId+"_"+strings.ReplaceAll(strings.ToLower(u.DisplayName), " ", "_"), string(encodedUser), -1); err != nil {
return err
}
}
if u.UidNumber != 0 {
if err = m.setVal(userPrefix+uidPrefix+strconv.FormatInt(u.UidNumber, 10), string(encodedUser), -1); err != nil {
return err
}
}
return nil
}
func (m *manager) fetchCachedUserByParam(field, claim string) (*userpb.User, error) {
user, err := m.getVal(userPrefix + field + ":" + strings.ToLower(claim))
if err != nil {
return nil, err
}
u := userpb.User{}
if err = json.Unmarshal([]byte(user), &u); err != nil {
return nil, err
}
return &u, nil
}
func (m *manager) fetchCachedUserGroups(uid *userpb.UserId) ([]string, error) {
groups, err := m.getVal(userPrefix + userGroupsPrefix + strings.ToLower(uid.OpaqueId))
if err != nil {
return nil, err
}
g := []string{}
if err = json.Unmarshal([]byte(groups), &g); err != nil {
return nil, err
}
return g, nil
}
func (m *manager) cacheUserGroups(uid *userpb.UserId, groups []string) error {
g, err := json.Marshal(&groups)
if err != nil {
return err
}
return m.setVal(userPrefix+userGroupsPrefix+strings.ToLower(uid.OpaqueId), string(g), m.conf.UserGroupsCacheExpiration*60)
}
+381
View File
@@ -0,0 +1,381 @@
// 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 rest
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/gomodule/redigo/redis"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
utils "github.com/opencloud-eu/reva/v2/pkg/cbox/utils"
"github.com/opencloud-eu/reva/v2/pkg/user"
"github.com/opencloud-eu/reva/v2/pkg/user/manager/registry"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
func init() {
registry.Register("rest", New)
}
type manager struct {
conf *config
redisPool *redis.Pool
apiTokenManager *utils.APITokenManager
}
type config struct {
// The address at which the redis server is running
RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
// The username for connecting to the redis server
RedisUsername string `mapstructure:"redis_username" docs:""`
// The password for connecting to the redis server
RedisPassword string `mapstructure:"redis_password" docs:""`
// The time in minutes for which the groups to which a user belongs would be cached
UserGroupsCacheExpiration int `mapstructure:"user_groups_cache_expiration" docs:"5"`
// The OIDC Provider
IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
// Base API Endpoint
APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
// Client ID needed to authenticate
ClientID string `mapstructure:"client_id" docs:"-"`
// Client Secret
ClientSecret string `mapstructure:"client_secret" docs:"-"`
// Endpoint to generate token to access the API
OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
// The target application for which token needs to be generated
TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
// The time in seconds between bulk fetch of user accounts
UserFetchInterval int `mapstructure:"user_fetch_interval" docs:"3600"`
}
func (c *config) init() {
if c.UserGroupsCacheExpiration == 0 {
c.UserGroupsCacheExpiration = 5
}
if c.RedisAddress == "" {
c.RedisAddress = ":6379"
}
if c.APIBaseURL == "" {
c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
}
if c.TargetAPI == "" {
c.TargetAPI = "authorization-service-api"
}
if c.OIDCTokenEndpoint == "" {
c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
}
if c.IDProvider == "" {
c.IDProvider = "http://cernbox.cern.ch"
}
if c.UserFetchInterval == 0 {
c.UserFetchInterval = 3600
}
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}
return c, nil
}
// New returns a user manager implementation that makes calls to the GRAPPA API.
func New(m map[string]interface{}) (user.Manager, error) {
mgr := &manager{}
err := mgr.Configure(m)
if err != nil {
return nil, err
}
return mgr, err
}
func (m *manager) Configure(ml map[string]interface{}) error {
c, err := parseConfig(ml)
if err != nil {
return err
}
c.init()
redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
m.conf = c
m.redisPool = redisPool
m.apiTokenManager = apiTokenManager
// Since we're starting a subroutine which would take some time to execute,
// we can't wait to see if it works before returning the user.Manager object
// TODO: return err if the fetch fails
go m.fetchAllUsers()
return nil
}
func (m *manager) fetchAllUsers() {
_ = m.fetchAllUserAccounts()
ticker := time.NewTicker(time.Duration(m.conf.UserFetchInterval) * 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.fetchAllUserAccounts()
}
}
}
func (m *manager) fetchAllUserAccounts() error {
ctx := context.Background()
url := fmt.Sprintf("%s/api/v1.0/Identity?field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type", m.conf.APIBaseURL)
for url != "" {
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
if err != nil {
return err
}
responseData, ok := result["data"].([]interface{})
if !ok {
return errors.New("rest: error in type assertion")
}
for _, usr := range responseData {
userData, ok := usr.(map[string]interface{})
if !ok {
continue
}
_, err = m.parseAndCacheUser(ctx, userData)
if err != nil {
continue
}
}
url = ""
if pagination, ok := result["pagination"].(map[string]interface{}); ok {
if links, ok := pagination["links"].(map[string]interface{}); ok {
if next, ok := links["next"].(string); ok {
url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
}
}
}
}
return nil
}
func (m *manager) parseAndCacheUser(ctx context.Context, userData map[string]interface{}) (*userpb.User, error) {
upn, ok := userData["upn"].(string)
if !ok {
return nil, errors.New("rest: missing upn in user data")
}
mail, _ := userData["primaryAccountEmail"].(string)
name, _ := userData["displayName"].(string)
uidNumber, _ := userData["uid"].(float64)
gidNumber, _ := userData["gid"].(float64)
t, _ := userData["type"].(string)
userType := getUserType(t, upn)
userID := &userpb.UserId{
OpaqueId: upn,
Idp: m.conf.IDProvider,
Type: userType,
}
u := &userpb.User{
Id: userID,
Username: upn,
Mail: mail,
DisplayName: name,
UidNumber: int64(uidNumber),
GidNumber: int64(gidNumber),
}
if err := m.cacheUserDetails(u); err != nil {
log.Error().Err(err).Msg("rest: error caching user details")
}
return u, nil
}
func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId, skipFetchingGroups bool) (*userpb.User, error) {
u, err := m.fetchCachedUserDetails(uid)
if err != nil {
return nil, err
}
if !skipFetchingGroups {
userGroups, err := m.GetUserGroups(ctx, uid)
if err != nil {
return nil, err
}
u.Groups = userGroups
}
return u, nil
}
func (m *manager) GetUserByClaim(ctx context.Context, claim, value string, skipFetchingGroups bool) (*userpb.User, error) {
u, err := m.fetchCachedUserByParam(claim, value)
if err != nil {
return nil, err
}
if !skipFetchingGroups {
userGroups, err := m.GetUserGroups(ctx, u.Id)
if err != nil {
return nil, err
}
u.Groups = userGroups
}
return u, nil
}
func (m *manager) FindUsers(ctx context.Context, query string, skipFetchingGroups bool) ([]*userpb.User, error) {
// Look at namespaces filters. If the query starts with:
// "a" => look into primary/secondary/service accounts
// "l" => look into lightweight/federated accounts
// none => look into primary
parts := strings.SplitN(query, ":", 2)
var namespace string
if len(parts) == 2 {
// the query contains a namespace filter
namespace, query = parts[0], parts[1]
}
users, err := m.findCachedUsers(query)
if err != nil {
return nil, err
}
userSlice := []*userpb.User{}
var accountsFilters []userpb.UserType
switch namespace {
case "":
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY}
case "a":
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_PRIMARY, userpb.UserType_USER_TYPE_SECONDARY, userpb.UserType_USER_TYPE_SERVICE}
case "l":
accountsFilters = []userpb.UserType{userpb.UserType_USER_TYPE_LIGHTWEIGHT, userpb.UserType_USER_TYPE_FEDERATED}
}
for _, u := range users {
if isUserAnyType(u, accountsFilters) {
userSlice = append(userSlice, u)
}
}
return userSlice, nil
}
// isUserAnyType returns true if the user's type is one of types list
func isUserAnyType(user *userpb.User, types []userpb.UserType) bool {
for _, t := range types {
if user.GetId().Type == t {
return true
}
}
return false
}
func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) {
groups, err := m.fetchCachedUserGroups(uid)
if err == nil {
return groups, nil
}
url := fmt.Sprintf("%s/api/v1.0/Identity/%s/groups?recursive=true", m.conf.APIBaseURL, uid.OpaqueId)
result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
if err != nil {
return nil, err
}
groupData := result["data"].([]interface{})
groups = []string{}
for _, g := range groupData {
groupInfo, ok := g.(map[string]interface{})
if !ok {
return nil, errors.New("rest: error in type assertion")
}
name, ok := groupInfo["displayName"].(string)
if ok {
groups = append(groups, name)
}
}
if err = m.cacheUserGroups(uid, groups); err != nil {
log := appctx.GetLogger(ctx)
log.Error().Err(err).Msg("rest: error caching user groups")
}
return groups, nil
}
func (m *manager) IsInGroup(ctx context.Context, uid *userpb.UserId, group string) (bool, error) {
userGroups, err := m.GetUserGroups(ctx, uid)
if err != nil {
return false, err
}
for _, g := range userGroups {
if group == g {
return true, nil
}
}
return false, nil
}
func getUserType(userType, upn string) userpb.UserType {
var t userpb.UserType
switch userType {
case "Application":
t = userpb.UserType_USER_TYPE_APPLICATION
case "Service":
t = userpb.UserType_USER_TYPE_SERVICE
case "Secondary":
t = userpb.UserType_USER_TYPE_SECONDARY
case "Person":
switch {
case strings.HasPrefix(upn, "guest"):
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
case strings.Contains(upn, "@"):
t = userpb.UserType_USER_TYPE_FEDERATED
default:
t = userpb.UserType_USER_TYPE_PRIMARY
}
default:
t = userpb.UserType_USER_TYPE_INVALID
}
return t
}
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2018-2023 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 utils
import (
"context"
"errors"
"time"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/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/conversions"
)
// DBShare stores information about user and public shares.
type DBShare struct {
ID string
UIDOwner string
UIDInitiator string
Prefix string
ItemSource string
ItemType string
ShareWith string
Token string
Expiration string
Permissions int
ShareType int
ShareName string
STime int
FileTarget string
State int
Quicklink bool
Description string
NotifyUploads bool
NotifyUploadsExtraRecipients string
}
// FormatGrantee formats a CS3API grantee to a string.
func FormatGrantee(g *provider.Grantee) (int, string) {
var granteeType int
var formattedID string
switch g.Type {
case provider.GranteeType_GRANTEE_TYPE_USER:
granteeType = 0
formattedID = FormatUserID(g.GetUserId())
case provider.GranteeType_GRANTEE_TYPE_GROUP:
granteeType = 1
formattedID = FormatGroupID(g.GetGroupId())
default:
granteeType = -1
}
return granteeType, formattedID
}
// ExtractGrantee retrieves the CS3API grantee from a formatted string.
func ExtractGrantee(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, t int, g string) (*provider.Grantee, error) {
var grantee provider.Grantee
switch t {
case 0:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
user, err := ExtractUserID(ctx, gateway, g)
if err != nil {
return nil, err
}
grantee.Id = &provider.Grantee_UserId{UserId: user}
case 1:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
group, err := ExtractGroupID(ctx, gateway, g)
if err != nil {
return nil, err
}
grantee.Id = &provider.Grantee_GroupId{GroupId: group}
default:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
}
return &grantee, nil
}
// ResourceTypeToItem maps a resource type to a string.
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 ""
}
}
// ResourceTypeToItemInt maps a resource type to an integer.
func ResourceTypeToItemInt(r provider.ResourceType) int {
switch r {
case provider.ResourceType_RESOURCE_TYPE_CONTAINER:
return 0
case provider.ResourceType_RESOURCE_TYPE_FILE:
return 1
default:
return -1
}
}
// SharePermToInt maps read/write permissions to an integer.
func SharePermToInt(p *provider.ResourcePermissions) int {
var perm int
switch {
case p.InitiateFileUpload && !p.InitiateFileDownload:
perm = 4
case p.InitiateFileUpload:
perm = 15
case p.InitiateFileDownload:
perm = 1
}
// TODO map denials and resharing; currently, denials are mapped to 0
return perm
}
// IntTosharePerm retrieves read/write permissions from an integer.
func IntTosharePerm(p int, itemType string) *provider.ResourcePermissions {
switch p {
case 1:
return conversions.NewViewerRole().CS3ResourcePermissions()
case 15:
if itemType == "folder" {
return conversions.NewEditorRole().CS3ResourcePermissions()
}
return conversions.NewFileEditorRole().CS3ResourcePermissions()
case 4:
return conversions.NewUploaderRole().CS3ResourcePermissions()
default:
// TODO we may have other options, for now this is a denial
return &provider.ResourcePermissions{}
}
}
// IntToShareState retrieves the received share state from an integer.
func IntToShareState(g int) collaboration.ShareState {
switch g {
case 0:
return collaboration.ShareState_SHARE_STATE_PENDING
case 1:
return collaboration.ShareState_SHARE_STATE_ACCEPTED
case -1:
return collaboration.ShareState_SHARE_STATE_REJECTED
default:
return collaboration.ShareState_SHARE_STATE_INVALID
}
}
// FormatUserID formats a CS3API user ID to a string.
func FormatUserID(u *userpb.UserId) string {
return u.OpaqueId
}
// ExtractUserID retrieves a CS3API user ID from a string.
func ExtractUserID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*userpb.UserId, error) {
userRes, err := gateway.GetUser(ctx, &userpb.GetUserRequest{
UserId: &userpb.UserId{OpaqueId: u},
})
if err != nil {
return nil, err
}
if userRes.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(userRes.Status.Message)
}
return userRes.User.Id, nil
}
// FormatGroupID formats a CS3API group ID to a string.
func FormatGroupID(u *grouppb.GroupId) string {
return u.OpaqueId
}
// ExtractGroupID retrieves a CS3API group ID from a string.
func ExtractGroupID(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, u string) (*grouppb.GroupId, error) {
groupRes, err := gateway.GetGroup(ctx, &grouppb.GetGroupRequest{
GroupId: &grouppb.GroupId{OpaqueId: u},
})
if err != nil {
return nil, err
}
if groupRes.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errors.New(groupRes.Status.Message)
}
return groupRes.Group.Id, nil
}
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share.
func ConvertToCS3Share(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.Share, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
if err != nil {
return nil, err
}
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
if err != nil {
return nil, err
}
grantee, err := ExtractGrantee(ctx, gateway, s.ShareType, s.ShareWith)
if err != nil {
return nil, err
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: s.ID,
},
//ResourceId: &provider.Reference{StorageId: s.Prefix, NodeId: s.ItemSource},
ResourceId: &provider.ResourceId{
StorageId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &collaboration.SharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Grantee: grantee,
Owner: owner,
Creator: creator,
Ctime: ts,
Mtime: ts,
}, nil
}
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share.
func ConvertToCS3ReceivedShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*collaboration.ReceivedShare, error) {
share, err := ConvertToCS3Share(ctx, gateway, s)
if err != nil {
return nil, err
}
return &collaboration.ReceivedShare{
Share: share,
State: IntToShareState(s.State),
}, nil
}
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share.
func ConvertToCS3PublicShare(ctx context.Context, gateway gatewayv1beta1.GatewayAPIClient, s DBShare) (*link.PublicShare, error) {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
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 {
expires = &typespb.Timestamp{
Seconds: uint64(t.Unix()),
}
}
}
owner, err := ExtractUserID(ctx, gateway, s.UIDOwner)
if err != nil {
return nil, err
}
creator, err := ExtractUserID(ctx, gateway, s.UIDInitiator)
if err != nil {
return nil, err
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
StorageId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Owner: owner,
Creator: creator,
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
Quicklink: s.Quicklink,
Description: s.Description,
NotifyUploads: s.NotifyUploads,
NotifyUploadsExtraRecipients: s.NotifyUploadsExtraRecipients,
}, nil
}
@@ -0,0 +1,172 @@
// 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 utils
import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
)
// APITokenManager stores config related to api management
type APITokenManager struct {
oidcToken OIDCToken
conf *config
client *http.Client
}
// OIDCToken stores the OIDC token used to authenticate requests to the REST API service
type OIDCToken struct {
sync.Mutex // concurrent access to apiToken and tokenExpirationTime
apiToken string
tokenExpirationTime time.Time
}
type config struct {
TargetAPI string
OIDCTokenEndpoint string
ClientID string
ClientSecret string
}
// InitAPITokenManager initializes a new APITokenManager
func InitAPITokenManager(targetAPI, oidcTokenEndpoint, clientID, clientSecret string) *APITokenManager {
return &APITokenManager{
conf: &config{
TargetAPI: targetAPI,
OIDCTokenEndpoint: oidcTokenEndpoint,
ClientID: clientID,
ClientSecret: clientSecret,
},
client: rhttp.GetHTTPClient(
rhttp.Timeout(10*time.Second),
rhttp.Insecure(true),
),
}
}
func (a *APITokenManager) renewAPIToken(ctx context.Context, forceRenewal bool) error {
// Received tokens have an expiration time of 20 minutes.
// Take a couple of seconds as buffer time for the API call to complete
if forceRenewal || a.oidcToken.tokenExpirationTime.Before(time.Now().Add(time.Second*time.Duration(2))) {
token, expiration, err := a.getAPIToken(ctx)
if err != nil {
return err
}
a.oidcToken.Lock()
defer a.oidcToken.Unlock()
a.oidcToken.apiToken = token
a.oidcToken.tokenExpirationTime = expiration
}
return nil
}
func (a *APITokenManager) getAPIToken(ctx context.Context) (string, time.Time, error) {
params := url.Values{
"grant_type": {"client_credentials"},
"audience": {a.conf.TargetAPI},
}
httpReq, err := http.NewRequest("POST", a.conf.OIDCTokenEndpoint, strings.NewReader(params.Encode()))
if err != nil {
return "", time.Time{}, err
}
httpReq.SetBasicAuth(a.conf.ClientID, a.conf.ClientSecret)
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
httpRes, err := a.client.Do(httpReq)
if err != nil {
return "", time.Time{}, err
}
defer httpRes.Body.Close()
body, err := io.ReadAll(httpRes.Body)
if err != nil {
return "", time.Time{}, err
}
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
return "", time.Time{}, errors.New("rest: get token endpoint returned " + httpRes.Status)
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return "", time.Time{}, err
}
expirationSecs := result["expires_in"].(float64)
expirationTime := time.Now().Add(time.Second * time.Duration(expirationSecs))
return result["access_token"].(string), expirationTime, nil
}
// SendAPIGetRequest makes an API GET Request to the passed URL
func (a *APITokenManager) SendAPIGetRequest(ctx context.Context, url string, forceRenewal bool) (map[string]interface{}, error) {
err := a.renewAPIToken(ctx, forceRenewal)
if err != nil {
return nil, err
}
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// We don't need to take the lock when reading apiToken, because if we reach here,
// the token is valid at least for a couple of seconds. Even if another request modifies
// the token and expiration time while this request is in progress, the current token will still be valid.
httpReq.Header.Set("Authorization", "Bearer "+a.oidcToken.apiToken)
httpRes, err := a.client.Do(httpReq)
if err != nil {
return nil, err
}
defer httpRes.Body.Close()
if httpRes.StatusCode == http.StatusUnauthorized {
// The token is no longer valid, try renewing it
return a.SendAPIGetRequest(ctx, url, true)
}
if httpRes.StatusCode < 200 || httpRes.StatusCode > 299 {
return nil, errors.New("rest: API request returned " + httpRes.Status)
}
body, err := io.ReadAll(httpRes.Body)
if err != nil {
return nil, err
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
return result, nil
}