Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
package cache
import (
"context"
"errors"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3Group "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/jellydator/ttlcache/v3"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/qsfera/server/services/graph/pkg/errorcode"
"github.com/qsfera/server/services/graph/pkg/identity"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
revautils "github.com/opencloud-eu/reva/v2/pkg/utils"
)
// IdentityCache implements a simple ttl based cache for looking up users and groups by ID
type IdentityCache struct {
users *ttlcache.Cache[string, *cs3User.User]
groups *ttlcache.Cache[string, libregraph.Group]
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
}
type identityCacheOptions struct {
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
usersTTL time.Duration
groupsTTL time.Duration
}
// IdentityCacheOption defines a single option function.
type IdentityCacheOption func(o *identityCacheOptions)
// IdentityCacheWithGatewaySelector set the gatewaySelector for the Identity Cache
func IdentityCacheWithGatewaySelector(gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.gatewaySelector = gatewaySelector
}
}
// IdentityCacheWithUsersTTL sets the TTL for the users cache
func IdentityCacheWithUsersTTL(ttl time.Duration) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.usersTTL = ttl
}
}
// IdentityCacheWithGroupsTTL sets the TTL for the groups cache
func IdentityCacheWithGroupsTTL(ttl time.Duration) IdentityCacheOption {
return func(o *identityCacheOptions) {
o.groupsTTL = ttl
}
}
func newOptions(opts ...IdentityCacheOption) identityCacheOptions {
opt := identityCacheOptions{}
for _, o := range opts {
o(&opt)
}
return opt
}
// NewIdentityCache instantiates a new IdentityCache and sets the supplied options
func NewIdentityCache(opts ...IdentityCacheOption) IdentityCache {
opt := newOptions(opts...)
var cache IdentityCache
cache.users = ttlcache.New(
ttlcache.WithTTL[string, *cs3user.User](opt.usersTTL),
ttlcache.WithDisableTouchOnHit[string, *cs3user.User](),
)
go cache.users.Start()
cache.groups = ttlcache.New(
ttlcache.WithTTL[string, libregraph.Group](opt.groupsTTL),
ttlcache.WithDisableTouchOnHit[string, libregraph.Group](),
)
go cache.groups.Start()
cache.gatewaySelector = opt.gatewaySelector
return cache
}
// GetUser looks up a user by id, if the user is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetUser(ctx context.Context, tenantId, userid string) (libregraph.User, error) {
// can we get the tenant from the context or do we have to pass it?
u, err := cache.GetCS3User(ctx, tenantId, userid)
if err != nil {
return libregraph.User{}, err
}
if tenantId != u.GetId().GetTenantId() {
return libregraph.User{}, identity.ErrNotFound
}
return *identity.CreateUserModelFromCS3(u), nil
}
func (cache IdentityCache) GetCS3User(ctx context.Context, tenantId, userid string) (*cs3User.User, error) {
var user *cs3User.User
if item := cache.users.Get(tenantId + "|" + userid); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cs3UserID := &cs3User.UserId{
OpaqueId: userid,
TenantId: tenantId,
}
user, err = revautils.GetUserNoGroups(ctx, cs3UserID, gatewayClient)
if err != nil {
if revautils.IsErrNotFound(err) {
return nil, identity.ErrNotFound
}
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cache.users.Set(tenantId+"|"+userid, user, ttlcache.DefaultTTL)
} else {
user = item.Value()
}
return user, nil
}
// GetAcceptedUser looks up a user by id, if the user is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetAcceptedUser(ctx context.Context, userid string) (libregraph.User, error) {
u, err := cache.GetAcceptedCS3User(ctx, userid)
if err != nil {
return libregraph.User{}, err
}
return *identity.CreateUserModelFromCS3(u), nil
}
func getIDAndMeshProvider(user string) (id, provider string, err error) {
last := strings.LastIndex(user, "@")
if last == -1 {
return "", "", errors.New("not in the form <id>@<provider>")
}
if len(user[:last]) == 0 {
return "", "", errors.New("empty id")
}
if len(user[last+1:]) == 0 {
return "", "", errors.New("empty provider")
}
return user[:last], user[last+1:], nil
}
func (cache IdentityCache) GetAcceptedCS3User(ctx context.Context, userid string) (*cs3User.User, error) {
var user *cs3user.User
if item := cache.users.Get(userid); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
id, provider, err := getIDAndMeshProvider(userid)
if err != nil {
return nil, errorcode.New(errorcode.InvalidRequest, err.Error())
}
cs3UserID := &cs3User.UserId{
Idp: provider,
OpaqueId: id,
Type: cs3User.UserType_USER_TYPE_FEDERATED,
}
user, err = revautils.GetAcceptedUserWithContext(ctx, cs3UserID, gatewayClient)
if err != nil {
if revautils.IsErrNotFound(err) {
return nil, identity.ErrNotFound
}
return nil, errorcode.New(errorcode.GeneralException, err.Error())
}
cache.users.Set(userid, user, ttlcache.DefaultTTL)
} else {
user = item.Value()
}
return user, nil
}
// GetGroup looks up a group by id, if the group is not cached, yet it will do a lookup via the CS3 API
func (cache IdentityCache) GetGroup(ctx context.Context, groupID string) (libregraph.Group, error) {
var group libregraph.Group
if item := cache.groups.Get(groupID); item == nil {
gatewayClient, err := cache.gatewaySelector.Next()
if err != nil {
return group, errorcode.New(errorcode.GeneralException, err.Error())
}
cs3GroupID := &cs3Group.GroupId{
OpaqueId: groupID,
}
req := cs3Group.GetGroupRequest{
GroupId: cs3GroupID,
SkipFetchingMembers: true,
}
res, err := gatewayClient.GetGroup(ctx, &req)
if err != nil {
return group, errorcode.New(errorcode.GeneralException, err.Error())
}
switch res.Status.Code {
case rpc.Code_CODE_OK:
g := res.GetGroup()
group = *identity.CreateGroupModelFromCS3(g)
cache.groups.Set(groupID, group, ttlcache.DefaultTTL)
case rpc.Code_CODE_NOT_FOUND:
return group, identity.ErrNotFound
default:
return group, errorcode.New(errorcode.GeneralException, res.Status.Message)
}
} else {
group = item.Value()
}
return group, nil
}
@@ -0,0 +1,13 @@
package cache_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestCache(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cache Suite")
}
+106
View File
@@ -0,0 +1,106 @@
package cache
import (
"context"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// mockGatewaySelector is a mock implementation of pool.Selectable[gateway.GatewayAPIClient]
type mockGatewaySelector struct {
client gateway.GatewayAPIClient
}
func (m *mockGatewaySelector) Next(opts ...pool.Option) (gateway.GatewayAPIClient, error) {
return m.client, nil
}
var _ = Describe("Cache", func() {
var (
ctx context.Context
idc IdentityCache
mockGwSelector pool.Selectable[gateway.GatewayAPIClient]
)
BeforeEach(func() {
// Create a mock gateway selector (client can be nil for cached tests)
mockGwSelector = &mockGatewaySelector{
client: nil,
}
idc = NewIdentityCache(
IdentityCacheWithGatewaySelector(mockGwSelector),
)
ctx = context.Background()
})
Describe("GetUser", func() {
It("should return no error", func() {
alan := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "",
},
DisplayName: "Alan",
}
// Persist the user to the cache for 1 hour
idc.users.Set(alan.GetId().GetTenantId()+"|"+alan.GetId().GetOpaqueId(), alan, time.Hour)
// getting the cache item in cache.go line 103 does not work
ru, err := idc.GetUser(ctx, "", "alan")
Expect(err).To(BeNil())
Expect(ru).ToNot(BeNil())
Expect(ru.GetId()).To(Equal(alan.GetId().GetOpaqueId()))
Expect(ru.GetDisplayName()).To(Equal(alan.GetDisplayName()))
})
It("should return the correct user if two users with the same uid and different tennant ids exist", func() {
alan1 := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "1234",
},
DisplayName: "Alan1",
}
alan2 := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "5678",
},
DisplayName: "Alan2",
}
// Persist the user to the cache for 1 hour
idc.users.Set(alan1.GetId().GetTenantId()+"|"+alan1.GetId().GetOpaqueId(), alan1, time.Hour)
idc.users.Set(alan2.GetId().GetTenantId()+"|"+alan2.GetId().GetOpaqueId(), alan2, time.Hour)
ru, err := idc.GetUser(ctx, "5678", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan2.GetDisplayName()))
ru, err = idc.GetUser(ctx, "1234", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan1.GetDisplayName()))
})
It("should not return an error, if the tenant id does match", func() {
alan := &cs3User.User{
Id: &cs3User.UserId{
OpaqueId: "alan",
TenantId: "1234",
},
DisplayName: "Alan",
}
// Persist the user to the cache for 1 hour
cu := idc.users.Set(alan.GetId().GetTenantId()+"|"+alan.GetId().GetOpaqueId(), alan, time.Hour)
// Test if element has been persisted in the cache
Expect(cu.Value().GetId().GetOpaqueId()).To(Equal(alan.GetId().GetOpaqueId()))
ru, err := idc.GetUser(ctx, "1234", "alan")
Expect(err).To(BeNil())
Expect(ru.GetDisplayName()).To(Equal(alan.GetDisplayName()))
})
})
})