Skip to content

Commit abfa912

Browse files
committed
Second iteration, yet fully AI-coded
1 parent a57bc13 commit abfa912

12 files changed

Lines changed: 1195 additions & 1016 deletions

File tree

cache/cache.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
redispools "github.com/cernbox/reva-plugins/redispools"
8+
"github.com/gomodule/redigo/redis"
9+
)
10+
11+
// store JSON-marshals v and writes it to Redis under key with a TTL in seconds.
12+
// Pass ttlSecs = -1 for no expiry.
13+
func store(pools *redispools.RedisPools, key string, v any, ttlSecs int) error {
14+
encoded, err := json.Marshal(v)
15+
if err != nil {
16+
return err
17+
}
18+
return pools.SetVal(key, string(encoded), ttlSecs)
19+
}
20+
21+
// fetch retrieves key from Redis and JSON-unmarshals the value into v.
22+
// Returns an error if the key is absent or the value cannot be decoded.
23+
func fetch(ctx context.Context, pools *redispools.RedisPools, key string, v any) error {
24+
raw, err := pools.GetVal(ctx, key)
25+
if err != nil {
26+
return err
27+
}
28+
return json.Unmarshal([]byte(raw), v)
29+
}
30+
31+
// scanAndFetch runs KEYS <pattern> followed by MGET and returns the raw
32+
// string values. Duplicate keys are collapsed by Redis itself. An empty
33+
// result set is not an error — callers get back an empty slice.
34+
func scanAndFetch(ctx context.Context, pools *redispools.RedisPools, pattern string) ([]string, error) {
35+
raw, err := pools.DoWithReadFallback(ctx, func(conn redis.Conn) (any, error) {
36+
keys, err := redis.Strings(conn.Do("KEYS", pattern))
37+
if err != nil {
38+
return nil, err
39+
}
40+
if len(keys) == 0 {
41+
return []string{}, nil
42+
}
43+
args := make([]any, len(keys))
44+
for i, k := range keys {
45+
args[i] = k
46+
}
47+
return redis.Strings(conn.Do("MGET", args...))
48+
})
49+
if err != nil {
50+
return nil, err
51+
}
52+
return raw.([]string), nil
53+
}

cache/group.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
redispools "github.com/cernbox/reva-plugins/redispools"
10+
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
11+
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
12+
"github.com/cs3org/reva/v3/pkg/appctx"
13+
"github.com/cs3org/reva/v3/pkg/errtypes"
14+
)
15+
16+
// Key schema:
17+
// group:id:<opaqueID> → JSON grouppb.Group
18+
// group:name:<opaqueID>_<display_lower_snake> → bare UUID string
19+
// group:members:<opaqueID> → JSON []*userpb.UserId
20+
21+
const (
22+
groupIDPrefix = "group:id:"
23+
groupNamePrefix = "group:name:"
24+
groupMembersPrefix = "group:members:"
25+
)
26+
27+
// GroupCache is a Redis-backed cache for CS3 group objects and their member lists.
28+
type GroupCache struct {
29+
pools *redispools.RedisPools
30+
groupTTLSecs int // 5 × fetch_interval
31+
membersTTLSecs int // member list TTL
32+
}
33+
34+
// NewGroupCache creates a GroupCache.
35+
// fetchInterval – seconds between full syncs
36+
// membersCacheMinutes – TTL for per-group member lists
37+
func NewGroupCache(pools *redispools.RedisPools, fetchInterval, membersCacheMinutes int) *GroupCache {
38+
return &GroupCache{
39+
pools: pools,
40+
groupTTLSecs: 5 * fetchInterval,
41+
membersTTLSecs: membersCacheMinutes * 60,
42+
}
43+
}
44+
45+
// StoreGroup writes a group under all applicable index keys.
46+
func (c *GroupCache) StoreGroup(g *grouppb.Group) error {
47+
if err := store(c.pools, groupIDPrefix+strings.ToLower(g.Id.OpaqueId), g, c.groupTTLSecs); err != nil {
48+
return err
49+
}
50+
if g.DisplayName != "" {
51+
nameKey := groupNamePrefix +
52+
strings.ToLower(g.Id.OpaqueId) + "_" +
53+
strings.ReplaceAll(strings.ToLower(g.DisplayName), " ", "_")
54+
// Store only the UUID; callers resolve it through GetByID.
55+
if err := store(c.pools, nameKey, g.Id.OpaqueId, c.groupTTLSecs); err != nil {
56+
return err
57+
}
58+
}
59+
return nil
60+
}
61+
62+
// GetByID looks up a group by its OpaqueId.
63+
func (c *GroupCache) GetByID(ctx context.Context, opaqueID string) (*grouppb.Group, error) {
64+
var g grouppb.Group
65+
if err := fetch(ctx, c.pools, groupIDPrefix+strings.ToLower(opaqueID), &g); err != nil {
66+
return nil, errtypes.NotFound(opaqueID)
67+
}
68+
return &g, nil
69+
}
70+
71+
// GetByName looks up a group by display name. It scans the name index to find
72+
// the UUID, then resolves the full object via GetByID.
73+
func (c *GroupCache) GetByName(ctx context.Context, name string) (*grouppb.Group, error) {
74+
pattern := fmt.Sprintf(
75+
"%s*_%s*",
76+
groupNamePrefix,
77+
strings.ReplaceAll(strings.ToLower(name), " ", "_"),
78+
)
79+
values, err := scanAndFetch(ctx, c.pools, pattern)
80+
if err != nil {
81+
return nil, err
82+
}
83+
if len(values) == 0 {
84+
return nil, errtypes.NotFound(name)
85+
}
86+
// values[0] is the bare UUID stored by StoreGroup.
87+
return c.GetByID(ctx, values[0])
88+
}
89+
90+
// Find scans all group index keys matching query and returns the deduplicated
91+
// set of groups. Entries may be either a JSON-encoded Group (from the primary
92+
// key) or a bare UUID string (from the name index); both are handled.
93+
func (c *GroupCache) Find(ctx context.Context, query string) ([]*grouppb.Group, error) {
94+
pattern := fmt.Sprintf(
95+
"group:*%s*",
96+
strings.ReplaceAll(strings.ToLower(query), " ", "_"),
97+
)
98+
values, err := scanAndFetch(ctx, c.pools, pattern)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
seen := make(map[string]*grouppb.Group)
104+
for _, s := range values {
105+
// Try to decode as a full Group first.
106+
var g grouppb.Group
107+
if err := json.Unmarshal([]byte(s), &g); err == nil && g.Id != nil {
108+
seen[g.Id.OpaqueId] = &g
109+
continue
110+
}
111+
// Otherwise it's a bare UUID from the name index — dereference it.
112+
if full, err := c.GetByID(ctx, s); err == nil {
113+
seen[full.Id.OpaqueId] = full
114+
}
115+
}
116+
117+
result := make([]*grouppb.Group, 0, len(seen))
118+
for _, g := range seen {
119+
result = append(result, g)
120+
}
121+
appctx.GetLogger(ctx).Debug().Str("pattern", pattern).Int("results", len(result)).Msg("cache: Find groups")
122+
return result, nil
123+
}
124+
125+
// StoreMembers writes the member list for a group.
126+
func (c *GroupCache) StoreMembers(gid *grouppb.GroupId, members []*userpb.UserId) error {
127+
return store(c.pools, groupMembersPrefix+strings.ToLower(gid.OpaqueId), members, c.membersTTLSecs)
128+
}
129+
130+
// GetMembers returns the cached member list for a group, or an error if the
131+
// entry is absent (so the caller can fall back to a live API fetch).
132+
func (c *GroupCache) GetMembers(ctx context.Context, opaqueID string) ([]*userpb.UserId, error) {
133+
var members []*userpb.UserId
134+
if err := fetch(ctx, c.pools, groupMembersPrefix+strings.ToLower(opaqueID), &members); err != nil {
135+
return nil, err
136+
}
137+
return members, nil
138+
}

cache/user.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
redispools "github.com/cernbox/reva-plugins/redispools"
10+
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
11+
"github.com/cs3org/reva/v3/pkg/appctx"
12+
"github.com/cs3org/reva/v3/pkg/errtypes"
13+
)
14+
15+
// Key schema:
16+
// user:id:<opaqueID> → JSON userpb.User
17+
// user:username:<login> → JSON userpb.User
18+
// user:mail:<email> → JSON userpb.User
19+
// user:name:<opaqueID>_<display_lower_snake> → JSON userpb.User
20+
// user:groups:<opaqueID> → JSON []string
21+
22+
const (
23+
userIDPrefix = "user:id:"
24+
userUsernamePrefix = "user:username:"
25+
userMailPrefix = "user:mail:"
26+
userNamePrefix = "user:name:"
27+
userGroupsPrefix = "user:groups:"
28+
)
29+
30+
// UserCache is a Redis-backed cache for CS3 user objects and their group
31+
// membership lists.
32+
type UserCache struct {
33+
pools *redispools.RedisPools
34+
userTTLSecs int // 5 × fetch_interval
35+
groupsTTLSecs int // group membership TTL
36+
}
37+
38+
// NewUserCache creates a UserCache.
39+
//
40+
// fetchInterval – seconds between full IAM/GRAPPA syncs (used to
41+
// derive user record TTL as 5× this value)
42+
// groupsCacheMinutes – TTL for per-user group membership lists
43+
func NewUserCache(pools *redispools.RedisPools, fetchInterval, groupsCacheMinutes int) *UserCache {
44+
return &UserCache{
45+
pools: pools,
46+
userTTLSecs: 5 * fetchInterval,
47+
groupsTTLSecs: groupsCacheMinutes * 60,
48+
}
49+
}
50+
51+
// StoreUser writes a user under all applicable index keys.
52+
func (c *UserCache) StoreUser(u *userpb.User) error {
53+
if err := store(c.pools, userIDPrefix+strings.ToLower(u.Id.OpaqueId), u, c.userTTLSecs); err != nil {
54+
return err
55+
}
56+
if u.Username != "" {
57+
if err := store(c.pools, userUsernamePrefix+strings.ToLower(u.Username), u, c.userTTLSecs); err != nil {
58+
return err
59+
}
60+
}
61+
if u.Mail != "" {
62+
if err := store(c.pools, userMailPrefix+strings.ToLower(u.Mail), u, c.userTTLSecs); err != nil {
63+
return err
64+
}
65+
}
66+
if u.DisplayName != "" {
67+
nameKey := userNamePrefix +
68+
strings.ToLower(u.Id.OpaqueId) + "_" +
69+
strings.ReplaceAll(strings.ToLower(u.DisplayName), " ", "_")
70+
if err := store(c.pools, nameKey, u, c.userTTLSecs); err != nil {
71+
return err
72+
}
73+
}
74+
return nil
75+
}
76+
77+
// GetByID looks up a user by their OpaqueId.
78+
func (c *UserCache) GetByID(ctx context.Context, opaqueID string) (*userpb.User, error) {
79+
var u userpb.User
80+
if err := fetch(ctx, c.pools, userIDPrefix+strings.ToLower(opaqueID), &u); err != nil {
81+
return nil, errtypes.NotFound(opaqueID)
82+
}
83+
return &u, nil
84+
}
85+
86+
// GetByUsername looks up a user by their login name.
87+
func (c *UserCache) GetByUsername(ctx context.Context, username string) (*userpb.User, error) {
88+
var u userpb.User
89+
if err := fetch(ctx, c.pools, userUsernamePrefix+strings.ToLower(username), &u); err != nil {
90+
return nil, errtypes.NotFound(username)
91+
}
92+
return &u, nil
93+
}
94+
95+
// GetByMail looks up a user by their primary email address.
96+
func (c *UserCache) GetByMail(ctx context.Context, mail string) (*userpb.User, error) {
97+
var u userpb.User
98+
if err := fetch(ctx, c.pools, userMailPrefix+strings.ToLower(mail), &u); err != nil {
99+
return nil, errtypes.NotFound(mail)
100+
}
101+
return &u, nil
102+
}
103+
104+
// Find scans all user index keys matching query and returns the deduplicated
105+
// set of users. An empty query returns all cached users.
106+
func (c *UserCache) Find(ctx context.Context, query string) ([]*userpb.User, error) {
107+
pattern := fmt.Sprintf(
108+
"user:*%s*",
109+
strings.ReplaceAll(strings.ToLower(query), " ", "_"),
110+
)
111+
values, err := scanAndFetch(ctx, c.pools, pattern)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
seen := make(map[string]*userpb.User)
117+
for _, s := range values {
118+
var u userpb.User
119+
// Only keep users that have no "inactive" status set (cf. parseAndCacheUser)
120+
if err := json.Unmarshal([]byte(s), &u); err == nil && u.Id != nil && u.Id.OpaqueId != "" && u.Status != userpb.UserStatus_USER_STATUS_EXPIRING {
121+
seen[u.Id.OpaqueId] = &u
122+
}
123+
}
124+
125+
result := make([]*userpb.User, 0, len(seen))
126+
for _, u := range seen {
127+
result = append(result, u)
128+
}
129+
appctx.GetLogger(ctx).Debug().Str("pattern", pattern).Int("results", len(result)).Msg("cache: Find users")
130+
return result, nil
131+
}
132+
133+
// StoreGroups writes the group membership list for a user.
134+
func (c *UserCache) StoreGroups(uid *userpb.UserId, groups []string) error {
135+
return store(c.pools, userGroupsPrefix+strings.ToLower(uid.OpaqueId), groups, c.groupsTTLSecs)
136+
}
137+
138+
// GetGroups returns the cached group membership list for a user, or an error
139+
// if the entry is absent (so the caller can fall back to a live API fetch).
140+
func (c *UserCache) GetGroups(ctx context.Context, opaqueID string) ([]string, error) {
141+
var groups []string
142+
if err := fetch(ctx, c.pools, userGroupsPrefix+strings.ToLower(opaqueID), &groups); err != nil {
143+
return nil, err
144+
}
145+
return groups, nil
146+
}

cernbox.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
// Add here all the plugins used by cernbox.
55
_ "github.com/cernbox/reva-plugins/cback/http"
66
_ "github.com/cernbox/reva-plugins/cback/storage"
7-
_ "github.com/cernbox/reva-plugins/group"
7+
_ "github.com/cernbox/reva-plugins/group/indigoiam"
8+
_ "github.com/cernbox/reva-plugins/group/rest"
89
_ "github.com/cernbox/reva-plugins/otg"
910
_ "github.com/cernbox/reva-plugins/storage/eoswrapper"
1011
_ "github.com/cernbox/reva-plugins/thumbnails"
11-
_ "github.com/cernbox/reva-plugins/user"
12+
_ "github.com/cernbox/reva-plugins/user/indigoiam"
13+
_ "github.com/cernbox/reva-plugins/user/rest"
1214
)

0 commit comments

Comments
 (0)