|
| 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 | +} |
0 commit comments