switch to go vendoring

This commit is contained in:
Michael Barz
2023-04-19 20:24:34 +02:00
parent 632fa05ef9
commit afc6ed1e41
8527 changed files with 3004916 additions and 2 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/cs3org/reva/v2/pkg/cbox/utils"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/storage/favorite"
"github.com/cs3org/reva/v2/pkg/storage/favorite/registry"
"github.com/mitchellh/mapstructure"
)
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/cs3org/reva/v2/pkg/appctx"
utils "github.com/cs3org/reva/v2/pkg/cbox/utils"
"github.com/cs3org/reva/v2/pkg/group"
"github.com/cs3org/reva/v2/pkg/group/manager/registry"
"github.com/gomodule/redigo/redis"
"github.com/mitchellh/mapstructure"
"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/cs3org/reva/v2/pkg/cbox/favorite/sql"
_ "github.com/cs3org/reva/v2/pkg/cbox/group/rest"
_ "github.com/cs3org/reva/v2/pkg/cbox/preferences/sql"
_ "github.com/cs3org/reva/v2/pkg/cbox/publicshare/sql"
_ "github.com/cs3org/reva/v2/pkg/cbox/share/sql"
_ "github.com/cs3org/reva/v2/pkg/cbox/storage/eoshomewrapper"
_ "github.com/cs3org/reva/v2/pkg/cbox/storage/eoswrapper"
_ "github.com/cs3org/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"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/preferences"
"github.com/cs3org/reva/v2/pkg/preferences/registry"
"github.com/mitchellh/mapstructure"
)
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
}
+509
View File
@@ -0,0 +1,509 @@
// 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"
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"
conversions "github.com/cs3org/reva/v2/pkg/cbox/utils"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/publicshare"
"github.com/cs3org/reva/v2/pkg/publicshare/manager/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"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"`
}
type manager struct {
c *config
db *sql.DB
}
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
}
mgr := manager{
c: c,
db: db,
}
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
}
return conversions.ConvertToCS3PublicShare(s), 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
}
return conversions.ConvertToCS3PublicShare(s), 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 := conversions.ConvertToCS3PublicShare(s)
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 := conversions.ConvertToCS3PublicShare(s)
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
}
+557
View File
@@ -0,0 +1,557 @@
// 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"
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"
conversions "github.com/cs3org/reva/v2/pkg/cbox/utils"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/share"
"github.com/cs3org/reva/v2/pkg/share/manager/registry"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"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"`
}
type mgr struct {
c *config
db *sql.DB
}
// 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
}
return &mgr{
c: c,
db: db,
}, 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
}
return conversions.ConvertToCS3Share(s), 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
}
return conversions.ConvertToCS3Share(s), 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
}
shares = append(shares, conversions.ConvertToCS3Share(s))
}
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) ([]*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
}
shares = append(shares, conversions.ConvertToCS3ReceivedShare(s))
}
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
}
return conversions.ConvertToCS3ReceivedShare(s), 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
}
return conversions.ConvertToCS3ReceivedShare(s), 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) (*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,128 @@
// 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"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/storage"
"github.com/cs3org/reva/v2/pkg/storage/fs/registry"
"github.com/cs3org/reva/v2/pkg/storage/utils/eosfs"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
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) (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,294 @@
// 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"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/storage"
"github.com/cs3org/reva/v2/pkg/storage/fs/registry"
"github.com/cs3org/reva/v2/pkg/storage/utils/eosfs"
"github.com/cs3org/reva/v2/pkg/storagespace"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
)
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) (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) (io.ReadCloser, error) {
if err := w.userIsProjectAdmin(ctx, ref); err != nil {
return nil, err
}
return w.FS.DownloadRevision(ctx, ref, revisionKey)
}
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/cs3org/reva/v2/pkg/appctx"
utils "github.com/cs3org/reva/v2/pkg/cbox/utils"
"github.com/cs3org/reva/v2/pkg/user"
"github.com/cs3org/reva/v2/pkg/user/manager/registry"
"github.com/gomodule/redigo/redis"
"github.com/mitchellh/mapstructure"
"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
}
+255
View File
@@ -0,0 +1,255 @@
// 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 (
"strings"
"time"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/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/cs3org/reva/v2/internal/http/services/owncloud/ocs/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
}
// 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(t int, g string) *provider.Grantee {
var grantee provider.Grantee
switch t {
case 0:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_USER
grantee.Id = &provider.Grantee_UserId{UserId: ExtractUserID(g)}
case 1:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_GROUP
grantee.Id = &provider.Grantee_GroupId{GroupId: ExtractGroupID(g)}
default:
grantee.Type = provider.GranteeType_GRANTEE_TYPE_INVALID
}
return &grantee
}
// 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(u string) *userpb.UserId {
t := userpb.UserType_USER_TYPE_PRIMARY
if strings.HasPrefix(u, "guest:") {
t = userpb.UserType_USER_TYPE_LIGHTWEIGHT
} else if strings.Contains(u, "@") {
t = userpb.UserType_USER_TYPE_FEDERATED
}
return &userpb.UserId{OpaqueId: u, Type: t}
}
// 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(u string) *grouppb.GroupId {
return &grouppb.GroupId{OpaqueId: u}
}
// ConvertToCS3Share converts a DBShare to a CS3API collaboration share
func ConvertToCS3Share(s DBShare) *collaboration.Share {
ts := &typespb.Timestamp{
Seconds: uint64(s.STime),
}
return &collaboration.Share{
Id: &collaboration.ShareId{
OpaqueId: s.ID,
},
//ResourceId: &provider.Reference{StorageId: s.Prefix, NodeId: s.ItemSource},
ResourceId: &provider.ResourceId{
SpaceId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &collaboration.SharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Grantee: ExtractGrantee(s.ShareType, s.ShareWith),
Owner: ExtractUserID(s.UIDOwner),
Creator: ExtractUserID(s.UIDInitiator),
Ctime: ts,
Mtime: ts,
}
}
// ConvertToCS3ReceivedShare converts a DBShare to a CS3API collaboration received share
func ConvertToCS3ReceivedShare(s DBShare) *collaboration.ReceivedShare {
return &collaboration.ReceivedShare{
Share: ConvertToCS3Share(s),
State: IntToShareState(s.State),
MountPoint: &provider.Reference{Path: strings.TrimLeft(s.FileTarget, "/")},
}
}
// ConvertToCS3PublicShare converts a DBShare to a CS3API public share
func ConvertToCS3PublicShare(s DBShare) *link.PublicShare {
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()),
}
}
}
return &link.PublicShare{
Id: &link.PublicShareId{
OpaqueId: s.ID,
},
ResourceId: &provider.ResourceId{
SpaceId: s.Prefix,
OpaqueId: s.ItemSource,
},
Permissions: &link.PublicSharePermissions{Permissions: IntTosharePerm(s.Permissions, s.ItemType)},
Owner: ExtractUserID(s.UIDOwner),
Creator: ExtractUserID(s.UIDInitiator),
Token: s.Token,
DisplayName: s.ShareName,
PasswordProtected: pwd,
Expiration: expires,
Ctime: ts,
Mtime: ts,
}
}
+172
View File
@@ -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/cs3org/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
}