switch to go vendoring
This commit is contained in:
+139
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Generated
Vendored
+128
@@ -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()
|
||||
}
|
||||
+294
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user