From 570554a06f57dcf23b616fde17c58ffff156d5c0 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Mon, 25 Sep 2023 12:09:50 +0200 Subject: [PATCH] removed cernbox plugins --- cmd/revad/runtime/loader.go | 1 - pkg/cbox/favorite/sql/sql.go | 136 ---- pkg/cbox/group/rest/cache.go | 214 ------ pkg/cbox/group/rest/rest.go | 311 --------- pkg/cbox/loader/loader.go | 31 - pkg/cbox/preferences/sql/sql.go | 100 --- pkg/cbox/publicshare/sql/sql.go | 607 ---------------- pkg/cbox/share/sql/sql.go | 648 ------------------ .../storage/eoshomewrapper/eoshomewrapper.go | 114 --- pkg/cbox/storage/eoswrapper/eoswrapper.go | 232 ------- pkg/cbox/user/rest/cache.go | 211 ------ pkg/cbox/user/rest/rest.go | 372 ---------- 12 files changed, 2977 deletions(-) delete mode 100644 pkg/cbox/favorite/sql/sql.go delete mode 100644 pkg/cbox/group/rest/cache.go delete mode 100644 pkg/cbox/group/rest/rest.go delete mode 100644 pkg/cbox/loader/loader.go delete mode 100644 pkg/cbox/preferences/sql/sql.go delete mode 100644 pkg/cbox/publicshare/sql/sql.go delete mode 100644 pkg/cbox/share/sql/sql.go delete mode 100644 pkg/cbox/storage/eoshomewrapper/eoshomewrapper.go delete mode 100644 pkg/cbox/storage/eoswrapper/eoswrapper.go delete mode 100644 pkg/cbox/user/rest/cache.go delete mode 100644 pkg/cbox/user/rest/rest.go diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index ad174b9130..163b66d823 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -33,7 +33,6 @@ import ( _ "github.com/cs3org/reva/pkg/appauth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/registry/loader" - _ "github.com/cs3org/reva/pkg/cbox/loader" _ "github.com/cs3org/reva/pkg/datatx/manager/loader" _ "github.com/cs3org/reva/pkg/group/manager/loader" _ "github.com/cs3org/reva/pkg/metrics/driver/loader" diff --git a/pkg/cbox/favorite/sql/sql.go b/pkg/cbox/favorite/sql/sql.go deleted file mode 100644 index 5e2ef9b238..0000000000 --- a/pkg/cbox/favorite/sql/sql.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package 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/pkg/cbox/utils" - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/storage/favorite" - "github.com/cs3org/reva/pkg/storage/favorite/registry" - "github.com/cs3org/reva/pkg/utils/cfg" -) - -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) { - var c config - if err := cfg.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.StorageId, &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) - - // 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, resourceInfo.Id.StorageId, 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, resourceInfo.Id.StorageId, 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) - 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, resourceInfo.Id.StorageId, resourceInfo.Id.OpaqueId) - if err != nil { - return err - } - - _, err = res.RowsAffected() - if err != nil { - return err - } - return nil -} diff --git a/pkg/cbox/group/rest/cache.go b/pkg/cbox/group/rest/cache.go deleted file mode 100644 index a9fb561678..0000000000 --- a/pkg/cbox/group/rest/cache.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package 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) 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 { - expiration := (m.conf.GroupFetchInterval + 1) * 3600 - encodedGroup, err := json.Marshal(&g) - if err != nil { - return err - } - if err = m.setVal(groupPrefix+idPrefix+strings.ToLower(g.Id.OpaqueId), string(encodedGroup), expiration); err != nil { - return err - } - - if g.GidNumber != 0 { - if err = m.setVal(groupPrefix+gidPrefix+strconv.FormatInt(g.GidNumber, 10), g.Id.OpaqueId, expiration); err != nil { - return err - } - } - if g.DisplayName != "" { - if err = m.setVal(groupPrefix+namePrefix+g.Id.OpaqueId+"_"+strings.ToLower(g.DisplayName), g.Id.OpaqueId, expiration); 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) -} diff --git a/pkg/cbox/group/rest/rest.go b/pkg/cbox/group/rest/rest.go deleted file mode 100644 index 43acf7a2a3..0000000000 --- a/pkg/cbox/group/rest/rest.go +++ /dev/null @@ -1,311 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package rest - -import ( - "context" - "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/pkg/appctx" - user "github.com/cs3org/reva/pkg/cbox/user/rest" - utils "github.com/cs3org/reva/pkg/cbox/utils" - "github.com/cs3org/reva/pkg/group" - "github.com/cs3org/reva/pkg/group/manager/registry" - "github.com/cs3org/reva/pkg/utils/cfg" - "github.com/cs3org/reva/pkg/utils/list" - "github.com/gomodule/redigo/redis" - "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) ApplyDefaults() { - 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 - } -} - -// New returns a user manager implementation that makes calls to the GRAPPA API. -func New(ctx context.Context, m map[string]interface{}) (group.Manager, error) { - var c config - if err := cfg.Decode(m, &c); err != nil { - return nil, err - } - - redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword) - apiTokenManager, err := utils.InitAPITokenManager(m) - if err != nil { - return nil, err - } - - mgr := &manager{ - conf: &c, - redisPool: redisPool, - apiTokenManager: apiTokenManager, - } - go mgr.fetchAllGroups(context.Background()) - return mgr, nil -} - -func (m *manager) fetchAllGroups(ctx context.Context) { - _ = m.fetchAllGroupAccounts(ctx) - 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(ctx) - } - } -} - -// Group contains the information about a group. -type Group struct { - GroupIdentifier string `json:"groupIdentifier"` - DisplayName string `json:"displayName"` - GID int `json:"gid,omitempty"` - IsComputingGroup bool `json:"isComputingGroup"` -} - -// GroupsResponse contains the expected response from grappa -// when getting the list of groups. -type GroupsResponse struct { - Pagination struct { - Links struct { - Next *string `json:"next"` - } `json:"links"` - } `json:"pagination"` - Data []*Group `json:"data"` -} - -func (m *manager) fetchAllGroupAccounts(ctx context.Context) error { - url := fmt.Sprintf("%s/api/v1.0/Group?field=groupIdentifier&field=displayName&field=gid&field=isComputingGroup", m.conf.APIBaseURL) - - for { - var r GroupsResponse - if err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false, &r); err != nil { - return err - } - - for _, g := range r.Data { - if g.IsComputingGroup { - continue - } - if _, err := m.parseAndCacheGroup(ctx, g); err != nil { - continue - } - } - - if r.Pagination.Links.Next == nil { - break - } - url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, *r.Pagination.Links.Next) - } - - return nil -} - -func (m *manager) parseAndCacheGroup(ctx context.Context, g *Group) (*grouppb.Group, error) { - groupID := &grouppb.GroupId{ - Idp: m.conf.IDProvider, - OpaqueId: g.GroupIdentifier, - } - - group := &grouppb.Group{ - Id: groupID, - GroupName: g.GroupIdentifier, - Mail: g.GroupIdentifier + "@cern.ch", - DisplayName: g.DisplayName, - GidNumber: int64(g.GID), - } - - if err := m.cacheGroupDetails(group); err != nil { - log.Error().Err(err).Msg("rest: error caching group details") - } - - return group, 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 - } - - url := fmt.Sprintf("%s/api/v1.0/Group/%s/memberidentities/precomputed?limit=10&field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type&field=source", m.conf.APIBaseURL, gid.OpaqueId) - - var r user.IdentitiesResponse - members := []*userpb.UserId{} - for { - if err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false, &r); err != nil { - return nil, err - } - - users := list.Map(r.Data, func(i *user.Identity) *userpb.UserId { - return &userpb.UserId{OpaqueId: i.Upn, Idp: m.conf.IDProvider, Type: i.UserType()} - }) - members = append(members, users...) - - if r.Pagination.Links.Next == nil { - break - } - url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, *r.Pagination.Links.Next) - } - - if err = m.cacheGroupMembers(gid, members); err != nil { - appctx.GetLogger(ctx).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) { - // TODO (gdelmont): this can be improved storing the users a group is composed of as a list in redis - // and, instead of returning all the members, use the redis apis to check if the user is in the list. - 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 -} diff --git a/pkg/cbox/loader/loader.go b/pkg/cbox/loader/loader.go deleted file mode 100644 index efeeb83db2..0000000000 --- a/pkg/cbox/loader/loader.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package loader - -import ( - // Load cbox specific drivers. - _ "github.com/cs3org/reva/pkg/cbox/favorite/sql" - _ "github.com/cs3org/reva/pkg/cbox/group/rest" - _ "github.com/cs3org/reva/pkg/cbox/preferences/sql" - _ "github.com/cs3org/reva/pkg/cbox/publicshare/sql" - _ "github.com/cs3org/reva/pkg/cbox/share/sql" - _ "github.com/cs3org/reva/pkg/cbox/storage/eoshomewrapper" - _ "github.com/cs3org/reva/pkg/cbox/storage/eoswrapper" - _ "github.com/cs3org/reva/pkg/cbox/user/rest" -) diff --git a/pkg/cbox/preferences/sql/sql.go b/pkg/cbox/preferences/sql/sql.go deleted file mode 100644 index 8e0b2afbd5..0000000000 --- a/pkg/cbox/preferences/sql/sql.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package sql - -import ( - "context" - "database/sql" - "fmt" - - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/preferences" - "github.com/cs3org/reva/pkg/preferences/registry" - "github.com/cs3org/reva/pkg/utils/cfg" -) - -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(ctx context.Context, m map[string]interface{}) (preferences.Manager, error) { - var c config - if err := cfg.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 -} diff --git a/pkg/cbox/publicshare/sql/sql.go b/pkg/cbox/publicshare/sql/sql.go deleted file mode 100644 index 3602224ab0..0000000000 --- a/pkg/cbox/publicshare/sql/sql.go +++ /dev/null @@ -1,607 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package sql - -import ( - "context" - "database/sql" - "fmt" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - - gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - conversions "github.com/cs3org/reva/pkg/cbox/utils" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/publicshare" - "github.com/cs3org/reva/pkg/publicshare/manager/registry" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/sharedconf" - "github.com/cs3org/reva/pkg/utils" - "github.com/cs3org/reva/pkg/utils/cfg" - "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" -) - -const ( - publicShareType = 3 - - projectInstancesPrefix = "newproject" - projectSpaceGroupsPrefix = "cernbox-project-" - projectSpaceAdminGroupsSuffix = "-admins" -) - -func init() { - registry.Register("sql", New) -} - -type config struct { - SharePasswordHashCost int `mapstructure:"password_hash_cost"` - JanitorRunInterval int `mapstructure:"janitor_run_interval"` - EnableExpiredSharesCleanup bool `mapstructure:"enable_expired_shares_cleanup"` - DBUsername string `mapstructure:"db_username"` - DBPassword string `mapstructure:"db_password"` - DBHost string `mapstructure:"db_host"` - DBPort int `mapstructure:"db_port"` - DBName string `mapstructure:"db_name"` - GatewaySvc string `mapstructure:"gatewaysvc"` -} - -type manager struct { - c *config - db *sql.DB - client gatewayv1beta1.GatewayAPIClient -} - -func (c *config) ApplyDefaults() { - if c.SharePasswordHashCost == 0 { - c.SharePasswordHashCost = 11 - } - if c.JanitorRunInterval == 0 { - c.JanitorRunInterval = 3600 - } - - c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) -} - -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(ctx context.Context, m map[string]interface{}) (publicshare.Manager, error) { - var c config - if err := cfg.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 - } - - gw, err := pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySvc)) - if err != nil { - return nil, err - } - - mgr := manager{ - c: &c, - db: db, - client: gw, - } - go mgr.startJanitorRun() - - return &mgr, nil -} - -func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *provider.ResourceInfo, g *link.Grant, description string, internal bool, notifyUploads bool, notifyUploadsExtraRecipients string) (*link.PublicShare, error) { - tkn := utils.RandString(15) - now := time.Now().Unix() - - quicklink, _ := strconv.ParseBool(rInfo.ArbitraryMetadata.Metadata["quicklink"]) - - 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.StorageId - 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=?,quicklink=?,description=?,internal=?,notify_uploads=?,notify_uploads_extra_recipients=?" - params := []interface{}{publicShareType, owner, creator, itemType, prefix, itemSource, fileSource, permissions, now, tkn, displayName, quicklink, description, internal, notifyUploads, notifyUploadsExtraRecipients} - - 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, - Quicklink: quicklink, - Description: description, - NotifyUploads: notifyUploads, - NotifyUploadsExtraRecipients: notifyUploadsExtraRecipients, - }, nil -} - -func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest, g *link.Grant) (*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 - } - case link.UpdatePublicShareRequest_Update_TYPE_DESCRIPTION: - paramsMap["description"] = req.Update.GetDescription() - case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADS: - paramsMap["notify_uploads"] = req.Update.GetNotifyUploads() - case link.UpdatePublicShareRequest_Update_TYPE_NOTIFYUPLOADSEXTRARECIPIENTS: - paramsMap["notify_uploads_extra_recipients"] = req.Update.GetNotifyUploadsExtraRecipients() - 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, quicklink, description, notify_uploads, notify_uploads_extra_recipients 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, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { - if err == sql.ErrNoRows { - return nil, "", errtypes.NotFound(token) - } - return nil, "", err - } - share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s) - if err != nil { - return nil, "", err - } - return share, s.ShareWith, nil -} - -func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) { - uid := conversions.FormatUserID(u.Id) - s := conversions.DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions, quicklink, description, notify_uploads, notify_uploads_extra_recipients 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, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { - if err == sql.ErrNoRows { - return nil, "", errtypes.NotFound(id.OpaqueId) - } - return nil, "", err - } - share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s) - if err != nil { - return nil, "", err - } - return share, s.ShareWith, nil -} - -func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) { - var s *link.PublicShare - var pw string - var err error - switch { - case ref.GetId() != nil: - s, pw, err = m.getByID(ctx, ref.GetId(), u) - case ref.GetToken() != "": - s, pw, err = m.getByToken(ctx, ref.GetToken(), u) - default: - err = errtypes.NotFound(ref.String()) - } - if err != nil { - return nil, err - } - - if expired(s) { - if err := m.cleanupExpiredShares(); err != nil { - return nil, err - } - return nil, errtypes.NotFound(ref.String()) - } - - if s.PasswordProtected && sign { - if err := publicshare.AddSignature(s, pw); err != nil { - return nil, err - } - } - - return s, nil -} - -func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) { - 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, quicklink, description, notify_uploads, notify_uploads_extra_recipients FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (share_type=?) AND internal=false" - var resourceFilters, ownerFilters, creatorFilters string - var resourceParams, ownerParams, creatorParams []interface{} - params := []interface{}{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().StorageId, 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...) - } - - uidOwnersQuery, uidOwnersParams, err := m.uidOwnerFilters(ctx, u, filters) - if err != nil { - return nil, err - } - params = append(params, uidOwnersParams...) - if uidOwnersQuery != "" { - query = fmt.Sprintf("%s AND (%s)", query, uidOwnersQuery) - } - - 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, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { - continue - } - cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s) - if err != nil { - return nil, err - } - if expired(cs3Share) { - _ = m.cleanupExpiredShares() - } else { - if cs3Share.PasswordProtected && sign { - if err := publicshare.AddSignature(cs3Share, s.ShareWith); err != nil { - continue - } - } - 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, quicklink, description, notify_uploads, notify_uploads_extra_recipients 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, &s.Quicklink, &s.Description, &s.NotifyUploads, &s.NotifyUploadsExtraRecipients); err != nil { - if err == sql.ErrNoRows { - return nil, errtypes.NotFound(token) - } - return nil, err - } - cs3Share, err := conversions.ConvertToCS3PublicShare(ctx, m.client, s) - if err != nil { - return nil, err - } - if expired(cs3Share) { - if err := m.cleanupExpiredShares(); err != nil { - return nil, err - } - return nil, errtypes.NotFound(token) - } - 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 - } - } - } - - 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 (m *manager) uidOwnerFilters(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter) (string, []interface{}, error) { - uid := conversions.FormatUserID(u.Id) - - query := "uid_owner=? or uid_initiator=?" - params := []interface{}{uid, uid} - - client, err := pool.GetGatewayServiceClient(pool.Endpoint(m.c.GatewaySvc)) - if err != nil { - return "", nil, err - } - - for _, f := range filters { - if f.Type == link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID { - // For shares inside project spaces, if the user is an admin, we try to list all shares created by other admins - if strings.HasPrefix(f.GetResourceId().GetStorageId(), projectInstancesPrefix) { - res, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: f.GetResourceId()}}) - if err != nil || res.Status.Code != rpc.Code_CODE_OK { - continue - } - - // The path will look like /eos/project/c/cernbox, we need to extract the project name - parts := strings.SplitN(res.Info.Path, "/", 6) - if len(parts) < 5 { - continue - } - - adminGroup := projectSpaceGroupsPrefix + parts[4] + projectSpaceAdminGroupsSuffix - for _, g := range u.Groups { - if g == adminGroup { - // User belongs to the admin group, list all shares for the resource - - // TODO: this only works if shares for a single project are requested. - // If shares for multiple projects are requested, then we're not checking if the - // user is an admin for all of those. We can append the query ` or uid_owner=?` - // for all the project owners, which works fine for new reva - // but won't work for revaold since there, we store the uid of the share creator as uid_owner. - // For this to work across the two versions, this change would have to be made in revaold - // but it won't be straightforward as there, the storage provider doesn't return the - // resource owners. - return "", []interface{}{}, nil - } - } - } - } - } - - return query, params, 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 -} diff --git a/pkg/cbox/share/sql/sql.go b/pkg/cbox/share/sql/sql.go deleted file mode 100644 index 0133ed8ddb..0000000000 --- a/pkg/cbox/share/sql/sql.go +++ /dev/null @@ -1,648 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package sql - -import ( - "context" - "database/sql" - "fmt" - "path" - "strconv" - "strings" - "time" - - gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - conversions "github.com/cs3org/reva/pkg/cbox/utils" - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/pkg/share" - "github.com/cs3org/reva/pkg/share/manager/registry" - "github.com/cs3org/reva/pkg/sharedconf" - "github.com/cs3org/reva/pkg/utils" - "github.com/cs3org/reva/pkg/utils/cfg" - - // Provides mysql drivers. - _ "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" - "google.golang.org/genproto/protobuf/field_mask" -) - -const ( - shareTypeUser = 0 - shareTypeGroup = 1 - - projectInstancesPrefix = "newproject" - projectSpaceGroupsPrefix = "cernbox-project-" - projectSpaceAdminGroupsSuffix = "-admins" -) - -func init() { - registry.Register("sql", New) -} - -type config struct { - DBUsername string `mapstructure:"db_username"` - DBPassword string `mapstructure:"db_password"` - DBHost string `mapstructure:"db_host"` - DBPort int `mapstructure:"db_port"` - DBName string `mapstructure:"db_name"` - GatewaySvc string `mapstructure:"gatewaysvc"` -} - -type mgr struct { - c *config - db *sql.DB - client gatewayv1beta1.GatewayAPIClient -} - -func (c *config) ApplyDefaults() { - c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) -} - -// New returns a new share manager. -func New(ctx context.Context, m map[string]interface{}) (share.Manager, error) { - var c config - if err := cfg.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 - } - - gw, err := pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySvc)) - if err != nil { - return nil, err - } - - return &mgr{ - c: &c, - db: db, - client: gw, - }, 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.StorageId - itemSource := md.Id.OpaqueId - fileSource, err := strconv.ParseUint(itemSource, 10, 64) - if err != nil { - // it can be the case that the item source may be a character string - // we leave fileSource blank in that case - fileSource = 0 - } - - stmtString := "insert into oc_share set share_type=?,uid_owner=?,uid_initiator=?,item_type=?,fileid_prefix=?,item_source=?,file_source=?,permissions=?,stime=?,share_with=?,file_target=?" - stmtValues := []interface{}{shareType, conversions.FormatUserID(md.Owner), conversions.FormatUserID(user.Id), itemType, prefix, itemSource, fileSource, permissions, now, shareWith, targetPath} - - stmt, err := m.db.Prepare(stmtString) - if err != nil { - return nil, err - } - result, err := stmt.Exec(stmtValues...) - if err != nil { - return nil, err - } - lastID, err := result.LastInsertId() - if err != nil { - return nil, err - } - - return &collaboration.Share{ - Id: &collaboration.ShareId{ - OpaqueId: strconv.FormatInt(lastID, 10), - }, - ResourceId: md.Id, - Permissions: g.Permissions, - Grantee: g.Grantee, - Owner: md.Owner, - Creator: user.Id, - Ctime: ts, - Mtime: ts, - }, nil -} - -func (m *mgr) getByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.Share, error) { - uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id) - s := conversions.DBShare{ID: id.OpaqueId} - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND id=? AND (uid_owner=? or uid_initiator=?)" - if err := m.db.QueryRow(query, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.STime, &s.Permissions, &s.ShareType); err != nil { - if err == sql.ErrNoRows { - return nil, errtypes.NotFound(id.OpaqueId) - } - return nil, err - } - share, err := conversions.ConvertToCS3Share(ctx, m.client, s) - if err != nil { - return nil, err - } - return share, nil -} - -func (m *mgr) getByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.Share, error) { - owner := conversions.FormatUserID(key.Owner) - uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id) - - s := conversions.DBShare{} - shareType, shareWith := conversions.FormatGrantee(key.Grantee) - query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" - if err := m.db.QueryRow(query, owner, key.ResourceId.StorageId, key.ResourceId.OpaqueId, shareType, shareWith, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { - if err == sql.ErrNoRows { - return nil, errtypes.NotFound(key.String()) - } - return nil, err - } - share, err := conversions.ConvertToCS3Share(ctx, m.client, s) - if err != nil { - return nil, err - } - return share, nil -} - -func (m *mgr) GetShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.Share, error) { - var s *collaboration.Share - var err error - switch { - case ref.GetId() != nil: - s, err = m.getByID(ctx, ref.GetId()) - case ref.GetKey() != nil: - s, err = m.getByKey(ctx, ref.GetKey()) - default: - err = errtypes.NotFound(ref.String()) - } - - if err != nil { - return nil, err - } - - return s, nil -} - -func (m *mgr) Unshare(ctx context.Context, ref *collaboration.ShareReference) error { - uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id) - var query string - params := []interface{}{} - switch { - case ref.GetId() != nil: - query = "delete from oc_share where id=? AND (uid_owner=? or uid_initiator=?)" - params = append(params, ref.GetId().OpaqueId, uid, uid) - case ref.GetKey() != nil: - key := ref.GetKey() - shareType, shareWith := conversions.FormatGrantee(key.Grantee) - owner := conversions.FormatUserID(key.Owner) - query = "delete from oc_share where uid_owner=? AND fileid_prefix=? AND item_source=? AND share_type=? AND share_with=? AND (uid_owner=? or uid_initiator=?)" - params = append(params, owner, key.ResourceId.StorageId, 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) (*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.StorageId, 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) { - 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 (share_type=? OR share_type=?)` - params := []interface{}{shareTypeUser, shareTypeGroup} - - groupedFilters := share.GroupFiltersByType(filters) - if len(groupedFilters) > 0 { - filterQuery, filterParams, err := translateFilters(groupedFilters) - if err != nil { - return nil, err - } - params = append(params, filterParams...) - if filterQuery != "" { - query = fmt.Sprintf("%s AND (%s)", query, filterQuery) - } - } - - uidOwnersQuery, uidOwnersParams, err := m.uidOwnerFilters(ctx, groupedFilters) - if err != nil { - return nil, err - } - params = append(params, uidOwnersParams...) - if uidOwnersQuery != "" { - query = fmt.Sprintf("%s AND (%s)", query, uidOwnersQuery) - } - - rows, err := m.db.Query(query, params...) - if err != nil { - return nil, err - } - defer rows.Close() - - var s conversions.DBShare - shares := []*collaboration.Share{} - for rows.Next() { - if err := rows.Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.ItemType, &s.ID, &s.STime, &s.Permissions, &s.ShareType); err != nil { - continue - } - share, err := conversions.ConvertToCS3Share(ctx, m.client, s) - if err != nil { - continue - } - shares = append(shares, share) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return shares, nil -} - -// we list the shares that are targeted to the user in context or to the user groups. -func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*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, - 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)" - } - - groupedFilters := share.GroupFiltersByType(filters) - filterQuery, filterParams, err := translateFilters(groupedFilters) - 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.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { - continue - } - share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s) - if err != nil { - continue - } - shares = append(shares, share) - } - if err = rows.Err(); err != nil { - return nil, err - } - - return shares, nil -} - -func (m *mgr) getReceivedByID(ctx context.Context, id *collaboration.ShareId) (*collaboration.ReceivedShare, error) { - user := ctxpkg.ContextMustGetUser(ctx) - uid := conversions.FormatUserID(user.Id) - - params := []interface{}{uid, id.OpaqueId, uid} - for _, v := range user.Groups { - params = append(params, v) - } - - s := conversions.DBShare{ID: id.OpaqueId} - query := `select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, - coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(item_type, '') as item_type, - 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.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { - if err == sql.ErrNoRows { - return nil, errtypes.NotFound(id.OpaqueId) - } - return nil, err - } - share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s) - if err != nil { - return nil, err - } - return share, nil -} - -func (m *mgr) getReceivedByKey(ctx context.Context, key *collaboration.ShareKey) (*collaboration.ReceivedShare, error) { - user := ctxpkg.ContextMustGetUser(ctx) - uid := conversions.FormatUserID(user.Id) - - shareType, shareWith := conversions.FormatGrantee(key.Grantee) - params := []interface{}{uid, conversions.FormatUserID(key.Owner), key.ResourceId.StorageId, 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, - 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.ID, &s.STime, &s.Permissions, &s.ShareType, &s.State); err != nil { - if err == sql.ErrNoRows { - return nil, errtypes.NotFound(key.String()) - } - return nil, err - } - - share, err := conversions.ConvertToCS3ReceivedShare(ctx, m.client, s) - if err != nil { - return nil, err - } - return share, nil -} - -func (m *mgr) GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) { - var s *collaboration.ReceivedShare - var err error - switch { - case ref.GetId() != nil: - s, err = m.getReceivedByID(ctx, ref.GetId()) - case ref.GetKey() != nil: - s, err = m.getReceivedByKey(ctx, ref.GetKey()) - default: - err = errtypes.NotFound(ref.String()) - } - - if err != nil { - return nil, err - } - - return s, nil -} - -func (m *mgr) UpdateReceivedShare(ctx context.Context, share *collaboration.ReceivedShare, fieldMask *field_mask.FieldMask) (*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 - 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 (m *mgr) uidOwnerFilters(ctx context.Context, filters map[collaboration.Filter_Type][]*collaboration.Filter) (string, []interface{}, error) { - user := ctxpkg.ContextMustGetUser(ctx) - uid := conversions.FormatUserID(user.Id) - - query := "uid_owner=? or uid_initiator=?" - params := []interface{}{uid, uid} - - client, err := pool.GetGatewayServiceClient(pool.Endpoint(m.c.GatewaySvc)) - if err != nil { - return "", nil, err - } - - if resourceFilters, ok := filters[collaboration.Filter_TYPE_RESOURCE_ID]; ok { - for _, f := range resourceFilters { - // For shares inside project spaces, if the user is an admin, we try to list all shares created by other admins - if strings.HasPrefix(f.GetResourceId().GetStorageId(), projectInstancesPrefix) { - res, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: f.GetResourceId()}}) - if err != nil || res.Status.Code != rpc.Code_CODE_OK { - continue - } - - // The path will look like /eos/project/c/cernbox, we need to extract the project name - parts := strings.SplitN(res.Info.Path, "/", 6) - if len(parts) < 5 { - continue - } - - adminGroup := projectSpaceGroupsPrefix + parts[4] + projectSpaceAdminGroupsSuffix - for _, g := range user.Groups { - if g == adminGroup { - // User belongs to the admin group, list all shares for the resource - - // TODO: this only works if shares for a single project are requested. - // If shares for multiple projects are requested, then we're not checking if the - // user is an admin for all of those. We can append the query ` or uid_owner=?` - // for all the project owners, which works fine for new reva - // but won't work for revaold since there, we store the uid of the share creator as uid_owner. - // For this to work across the two versions, this change would have to be made in revaold - // but it won't be straightforward as there, the storage provider doesn't return the - // resource owners. - return "", []interface{}{}, nil - } - } - } - } - } - - return query, params, 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 map[collaboration.Filter_Type][]*collaboration.Filter) (string, []interface{}, error) { - var ( - filterQuery string - params []interface{} - ) - - // 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, currFilters := range filters { - switch filterType { - case collaboration.Filter_TYPE_RESOURCE_ID: - filterQuery += "(" - for i, f := range currFilters { - filterQuery += "(fileid_prefix =? AND item_source=?)" - params = append(params, f.GetResourceId().StorageId, f.GetResourceId().OpaqueId) - - if i != len(currFilters)-1 { - filterQuery += " OR " - } - } - filterQuery += ")" - case collaboration.Filter_TYPE_GRANTEE_TYPE: - filterQuery += "(" - for i, f := range currFilters { - filterQuery += "share_type=?" - params = append(params, granteeTypeToShareType(f.GetGranteeType())) - - if i != len(currFilters)-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(filters)-1 { - filterQuery += " AND " - } - filterCounter++ - } - return filterQuery, params, nil -} diff --git a/pkg/cbox/storage/eoshomewrapper/eoshomewrapper.go b/pkg/cbox/storage/eoshomewrapper/eoshomewrapper.go deleted file mode 100644 index ec385dcef4..0000000000 --- a/pkg/cbox/storage/eoshomewrapper/eoshomewrapper.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package 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/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/storage" - "github.com/cs3org/reva/pkg/storage/fs/registry" - "github.com/cs3org/reva/pkg/storage/utils/eosfs" - "github.com/cs3org/reva/pkg/utils/cfg" -) - -func init() { - registry.Register("eoshomewrapper", New) -} - -type wrapper struct { - storage.FS - mountIDTemplate *template.Template -} - -// New returns an implementation of the storage.FS interface that forms a wrapper -// around separate connections to EOS. -func New(ctx context.Context, m map[string]interface{}) (storage.FS, error) { - var c eosfs.Config - if err := cfg.Decode(m, &c); err != nil { - 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}}" - } - - eos, err := eosfs.NewEOSFS(ctx, &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) (*provider.ResourceInfo, error) { - res, err := w.FS.GetMD(ctx, ref, mdKeys) - 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 []string) ([]*provider.ResourceInfo, error) { - res, err := w.FS.ListFolder(ctx, ref, mdKeys) - 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() -} diff --git a/pkg/cbox/storage/eoswrapper/eoswrapper.go b/pkg/cbox/storage/eoswrapper/eoswrapper.go deleted file mode 100644 index d4b518db40..0000000000 --- a/pkg/cbox/storage/eoswrapper/eoswrapper.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package eoswrapper - -import ( - "bytes" - "context" - "io" - "strings" - "text/template" - - "github.com/Masterminds/sprig" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - ctxpkg "github.com/cs3org/reva/pkg/ctx" - "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/storage" - "github.com/cs3org/reva/pkg/storage/fs/registry" - "github.com/cs3org/reva/pkg/storage/utils/eosfs" - "github.com/cs3org/reva/pkg/utils/cfg" -) - -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 -} - -// New returns an implementation of the storage.FS interface that forms a wrapper -// around separate connections to EOS. -func New(ctx context.Context, m map[string]interface{}) (storage.FS, error) { - var c eosfs.Config - if err := cfg.Decode(m, &c); err != nil { - 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 }}" - } - - eos, err := eosfs.NewEOSFS(ctx, &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 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) (*provider.ResourceInfo, error) { - res, err := w.FS.GetMD(ctx, ref, mdKeys) - 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 - } - - return res, nil -} - -func (w *wrapper) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) ([]*provider.ResourceInfo, error) { - res, err := w.FS.ListFolder(ctx, ref, mdKeys) - if err != nil { - return nil, err - } - for _, r := range res { - r.Id.StorageId = w.getMountID(ctx, r) - if err = w.setProjectSharingPermissions(ctx, r); err != nil { - continue - } - } - 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 "" - } - b := bytes.Buffer{} - if err := w.mountIDTemplate.Execute(&b, r); err != nil { - return "" - } - 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) - 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") -} diff --git a/pkg/cbox/user/rest/cache.go b/pkg/cbox/user/rest/cache.go deleted file mode 100644 index 6b6f12ac64..0000000000 --- a/pkg/cbox/user/rest/cache.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package 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:" - 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 { - expiration := (m.conf.UserFetchInterval + 1) * 3600 - encodedUser, err := json.Marshal(&u) - if err != nil { - return err - } - if err = m.setVal(userPrefix+usernamePrefix+strings.ToLower(u.Id.OpaqueId), string(encodedUser), expiration); err != nil { - return err - } - - if u.Mail != "" { - if err = m.setVal(userPrefix+mailPrefix+strings.ToLower(u.Mail), string(encodedUser), expiration); err != nil { - return err - } - } - if u.DisplayName != "" { - if err = m.setVal(userPrefix+namePrefix+u.Id.OpaqueId+"_"+strings.ReplaceAll(strings.ToLower(u.DisplayName), " ", "_"), string(encodedUser), expiration); err != nil { - return err - } - } - if u.UidNumber != 0 { - if err = m.setVal(userPrefix+uidPrefix+strconv.FormatInt(u.UidNumber, 10), string(encodedUser), expiration); 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) -} diff --git a/pkg/cbox/user/rest/rest.go b/pkg/cbox/user/rest/rest.go deleted file mode 100644 index d15ed7eeab..0000000000 --- a/pkg/cbox/user/rest/rest.go +++ /dev/null @@ -1,372 +0,0 @@ -// Copyright 2018-2023 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package rest - -import ( - "context" - "fmt" - "os" - "os/signal" - "strings" - "syscall" - "time" - - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - "github.com/cs3org/reva/pkg/appctx" - utils "github.com/cs3org/reva/pkg/cbox/utils" - "github.com/cs3org/reva/pkg/user" - "github.com/cs3org/reva/pkg/user/manager/registry" - "github.com/cs3org/reva/pkg/utils/cfg" - "github.com/cs3org/reva/pkg/utils/list" - "github.com/gomodule/redigo/redis" - "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) ApplyDefaults() { - 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 - } -} - -// New returns a user manager implementation that makes calls to the GRAPPA API. -func New(ctx context.Context, 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 { - var c config - if err := cfg.Decode(ml, &c); err != nil { - return err - } - redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword) - apiTokenManager, err := utils.InitAPITokenManager(ml) - if err != nil { - return err - } - 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(context.Background()) - return nil -} - -func (m *manager) fetchAllUsers(ctx context.Context) { - _ = m.fetchAllUserAccounts(ctx) - 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(ctx) - } - } -} - -// Identity contains the information of a single user. -type Identity struct { - PrimaryAccountEmail string `json:"primaryAccountEmail,omitempty"` - Type string `json:"type,omitempty"` - Upn string `json:"upn"` - DisplayName string `json:"displayName"` - Source string `json:"source,omitempty"` - UID int `json:"uid,omitempty"` - GID int `json:"gid,omitempty"` -} - -// IdentitiesResponse contains the expected response from grappa -// when getting the list of users. -type IdentitiesResponse struct { - Pagination struct { - Links struct { - Next *string `json:"next"` - } `json:"links"` - } `json:"pagination"` - Data []*Identity `json:"data"` -} - -// UserType convert the user type in grappa to CS3APIs. -func (i *Identity) UserType() userpb.UserType { - switch i.Type { - case "Application": - return userpb.UserType_USER_TYPE_APPLICATION - case "Service": - return userpb.UserType_USER_TYPE_SERVICE - case "Secondary": - return userpb.UserType_USER_TYPE_SECONDARY - case "Person": - if i.Source == "cern" { - return userpb.UserType_USER_TYPE_PRIMARY - } - return userpb.UserType_USER_TYPE_LIGHTWEIGHT - default: - return userpb.UserType_USER_TYPE_INVALID - } -} - -func (m *manager) fetchAllUserAccounts(ctx context.Context) error { - url := fmt.Sprintf("%s/api/v1.0/Identity?field=upn&field=primaryAccountEmail&field=displayName&field=uid&field=gid&field=type&field=source", m.conf.APIBaseURL) - - for { - var r IdentitiesResponse - if err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false, &r); err != nil { - return err - } - - for _, usr := range r.Data { - if _, err := m.parseAndCacheUser(ctx, usr); err != nil { - continue - } - } - - if r.Pagination.Links.Next == nil { - break - } - url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, *r.Pagination.Links.Next) - } - - return nil -} - -func (m *manager) parseAndCacheUser(ctx context.Context, i *Identity) (*userpb.User, error) { - u := &userpb.User{ - Id: &userpb.UserId{ - OpaqueId: i.Upn, - Idp: m.conf.IDProvider, - Type: i.UserType(), - }, - Username: i.Upn, - Mail: i.PrimaryAccountEmail, - DisplayName: i.DisplayName, - UidNumber: int64(i.UID), - GidNumber: int64(i.GID), - } - - 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 -} - -// Group contains the information about a group. -type Group struct { - DisplayName string `json:"displayName"` -} - -// GroupsResponse contains the expected response from grappa -// when getting the list of groups. -type GroupsResponse struct { - Pagination struct { - Links struct { - Next *string `json:"next"` - } `json:"links"` - } `json:"pagination"` - Data []Group `json:"data"` -} - -func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { - groups, err := m.fetchCachedUserGroups(uid) - if err == nil { - return groups, nil - } - - // TODO (gdelmont): support pagination! we may have problems with users having more than 1000 groups - url := fmt.Sprintf("%s/api/v1.0/Identity/%s/groups?field=displayName&recursive=true", m.conf.APIBaseURL, uid.OpaqueId) - - var r GroupsResponse - if err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false, &r); err != nil { - return nil, err - } - - groups = list.Map(r.Data, func(g Group) string { return g.DisplayName }) - - 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) { - // TODO (gdelmont): this can be improved storing the groups a user belong to as a list in redis - // and, instead of returning all the groups, use the redis apis to check if the group is in the list. - 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 -}