feat(user): options to disable exposing user emails to other users

pull/3490/head
Aaron Liu 3 months ago
parent 9e9fb43e72
commit ba2e870bbd

@ -1 +1 @@
Subproject commit 0b722766f8808b0a0aaa3c9f3e4766972d746d22 Subproject commit 807360a8a6646a1c4bf8f1151e75da17a3eb457c

@ -685,6 +685,7 @@ var DefaultSettings = map[string]string{
"fts_tika_max_file_size": "26214400", "fts_tika_max_file_size": "26214400",
"fts_chunk_size": "2000", "fts_chunk_size": "2000",
"viewer_default_apps": "{}", "viewer_default_apps": "{}",
"expose_user_email": "1",
} }
var RedactedSettings = map[string]struct{}{ var RedactedSettings = map[string]struct{}{

@ -236,6 +236,9 @@ type (
FTSChunkSize(ctx context.Context) int FTSChunkSize(ctx context.Context) int
// DefaultViewerMapping returns the default viewer mapping. // DefaultViewerMapping returns the default viewer mapping.
DefaultViewerMapping(ctx context.Context) types.DefaultViewerMapping DefaultViewerMapping(ctx context.Context) types.DefaultViewerMapping
// ExposeUserEmail returns true if user email should be exposed to other
// signed-in users in redacted responses.
ExposeUserEmail(ctx context.Context) bool
} }
UseFirstSiteUrlCtxKey = struct{} UseFirstSiteUrlCtxKey = struct{}
) )
@ -860,6 +863,10 @@ func (s *settingProvider) RegisterEnabled(ctx context.Context) bool {
return s.getBoolean(ctx, "register_enabled", false) return s.getBoolean(ctx, "register_enabled", false)
} }
func (s *settingProvider) ExposeUserEmail(ctx context.Context) bool {
return s.getBoolean(ctx, "expose_user_email", true)
}
func (s *settingProvider) SiteBasic(ctx context.Context) *SiteBasic { func (s *settingProvider) SiteBasic(ctx context.Context) *SiteBasic {
return &SiteBasic{ return &SiteBasic{
Name: s.getString(ctx, "siteName", ""), Name: s.getString(ctx, "siteName", ""),

@ -208,7 +208,7 @@ func UserGet(c *gin.Context) {
redactLevel = user.RedactLevelAnonymous redactLevel = user.RedactLevelAnonymous
} }
c.JSON(200, serializer.Response{ c.JSON(200, serializer.Response{
Data: user.BuildUserRedacted(u, redactLevel, dependency.FromContext(c).HashIDEncoder()), Data: user.BuildUserRedacted(c, u, redactLevel, dependency.FromContext(c).HashIDEncoder()),
}) })
} }
@ -368,7 +368,7 @@ func UserSearch(c *gin.Context) {
hasher := dependency.FromContext(c).HashIDEncoder() hasher := dependency.FromContext(c).HashIDEncoder()
c.JSON(200, serializer.Response{ c.JSON(200, serializer.Response{
Data: lo.Map(u, func(item *ent.User, index int) user.User { Data: lo.Map(u, func(item *ent.User, index int) user.User {
return user.BuildUserRedacted(item, user.RedactLevelUser, hasher) return user.BuildUserRedacted(c, item, user.RedactLevelUser, hasher)
}), }),
}) })
} }

@ -343,7 +343,7 @@ type Share struct {
SourceUri string `json:"source_uri,omitempty"` SourceUri string `json:"source_uri,omitempty"`
} }
func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *ent.User, owner *ent.User, func BuildShare(ctx context.Context, s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *ent.User, owner *ent.User,
name string, t types.FileType, unlocked bool, expired bool) *Share { name string, t types.FileType, unlocked bool, expired bool) *Share {
redactLevel := user.RedactLevelAnonymous redactLevel := user.RedactLevelAnonymous
if !inventory.IsAnonymousUser(requester) { if !inventory.IsAnonymousUser(requester) {
@ -353,7 +353,7 @@ func BuildShare(s *ent.Share, base *url.URL, hasher hashid.Encoder, requester *e
Name: name, Name: name,
ID: hashid.EncodeShareID(hasher, s.ID), ID: hashid.EncodeShareID(hasher, s.ID),
Unlocked: unlocked, Unlocked: unlocked,
Owner: user.BuildUserRedacted(owner, redactLevel, hasher), Owner: user.BuildUserRedacted(ctx, owner, redactLevel, hasher),
Expired: inventory.IsShareExpired(s) != nil || expired, Expired: inventory.IsShareExpired(s) != nil || expired,
Url: BuildShareLink(s, hasher, base, unlocked), Url: BuildShareLink(s, hasher, base, unlocked),
CreatedAt: s.CreatedAt, CreatedAt: s.CreatedAt,
@ -446,7 +446,7 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi
StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher), StoragePolicy: BuildStoragePolicy(extendedInfo.StoragePolicy, hasher),
StorageUsed: extendedInfo.StorageUsed, StorageUsed: extendedInfo.StorageUsed,
Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity { Entities: lo.Map(f.Entities(), func(e fs.Entity, index int) Entity {
return BuildEntity(extendedInfo, e, hasher) return BuildEntity(ctx, extendedInfo, e, hasher)
}), }),
DirectLinks: lo.Map(extendedInfo.DirectLinks, func(d *ent.DirectLink, index int) DirectLink { DirectLinks: lo.Map(extendedInfo.DirectLinks, func(d *ent.DirectLink, index int) DirectLink {
return BuildDirectLink(d, hasher, base) return BuildDirectLink(d, hasher, base)
@ -456,7 +456,7 @@ func BuildExtendedInfo(ctx context.Context, u *ent.User, f fs.File, hasher hashi
if u.ID == f.OwnerID() { if u.ID == f.OwnerID() {
// Only owner can see the shares settings. // Only owner can see the shares settings.
ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share { ext.Shares = lo.Map(extendedInfo.Shares, func(s *ent.Share, index int) Share {
return *BuildShare(s, base, hasher, u, u, f.DisplayName(), f.Type(), true, false) return *BuildShare(ctx, s, base, hasher, u, u, f.DisplayName(), f.Type(), true, false)
}) })
ext.View = extendedInfo.View ext.View = extendedInfo.View
} }
@ -473,11 +473,11 @@ func BuildDirectLink(d *ent.DirectLink, hasher hashid.Encoder, base *url.URL) Di
} }
} }
func BuildEntity(extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity { func BuildEntity(ctx context.Context, extendedInfo *fs.FileExtendedInfo, e fs.Entity, hasher hashid.Encoder) Entity {
var u *user.User var u *user.User
createdBy := e.CreatedBy() createdBy := e.CreatedBy()
if createdBy != nil { if createdBy != nil {
userRedacted := user.BuildUserRedacted(e.CreatedBy(), user.RedactLevelAnonymous, hasher) userRedacted := user.BuildUserRedacted(ctx, e.CreatedBy(), user.RedactLevelAnonymous, hasher)
u = &userRedacted u = &userRedacted
} }

@ -1,6 +1,7 @@
package share package share
import ( import (
"context"
"net/url" "net/url"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
@ -18,7 +19,7 @@ type ListShareResponse struct {
Pagination *inventory.PaginationResults `json:"pagination"` Pagination *inventory.PaginationResults `json:"pagination"`
} }
func BuildListShareResponse(res *inventory.ListShareResult, hasher hashid.Encoder, base *url.URL, requester *ent.User, unlocked bool) *ListShareResponse { func BuildListShareResponse(ctx context.Context, res *inventory.ListShareResult, hasher hashid.Encoder, base *url.URL, requester *ent.User, unlocked bool) *ListShareResponse {
var infos []explorer.Share var infos []explorer.Share
for _, share := range res.Shares { for _, share := range res.Shares {
expired := inventory.IsValidShare(share) != nil expired := inventory.IsValidShare(share) != nil
@ -36,7 +37,7 @@ func BuildListShareResponse(res *inventory.ListShareResult, hasher hashid.Encode
} }
} }
infos = append(infos, *explorer.BuildShare(share, base, hasher, requester, share.Edges.User, shareName, infos = append(infos, *explorer.BuildShare(ctx, share, base, hasher, requester, share.Edges.User, shareName,
types.FileType(share.Edges.File.Type), unlocked, expired)) types.FileType(share.Edges.File.Type), unlocked, expired))
} }

@ -87,7 +87,7 @@ func (s *ShareInfoService) Get(c *gin.Context) (*explorer.Share, error) {
} }
base := dep.SettingProvider().SiteURL(c) base := dep.SettingProvider().SiteURL(c)
res := explorer.BuildShare(share, base, dep.HashIDEncoder(), u, share.Edges.User, share.Edges.File.Name, res := explorer.BuildShare(c, share, base, dep.HashIDEncoder(), u, share.Edges.User, share.Edges.File.Name,
types.FileType(share.Edges.File.Type), unlocked, false) types.FileType(share.Edges.File.Type), unlocked, false)
if s.OwnerExtended && share.Edges.User.ID == u.ID { if s.OwnerExtended && share.Edges.User.ID == u.ID {
@ -148,7 +148,7 @@ func (s *ListShareService) List(c *gin.Context) (*ListShareResponse, error) {
} }
base := dep.SettingProvider().SiteURL(ctx) base := dep.SettingProvider().SiteURL(ctx)
return BuildListShareResponse(res, hasher, base, user, true), nil return BuildListShareResponse(ctx, res, hasher, base, user, true), nil
} }
func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShareResponse, error) { func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShareResponse, error) {
@ -188,5 +188,5 @@ func (s *ListShareService) ListInUserProfile(c *gin.Context, uid int) (*ListShar
} }
base := dep.SettingProvider().SiteURL(ctx) base := dep.SettingProvider().SiteURL(ctx)
return BuildListShareResponse(res, hasher, base, user, false), nil return BuildListShareResponse(ctx, res, hasher, base, user, false), nil
} }

@ -1,9 +1,11 @@
package user package user
import ( import (
"context"
"fmt" "fmt"
"time" "time"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent" "github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/ent/user" "github.com/cloudreve/Cloudreve/v4/ent/user"
"github.com/cloudreve/Cloudreve/v4/inventory/types" "github.com/cloudreve/Cloudreve/v4/inventory/types"
@ -222,7 +224,9 @@ const (
) )
// BuildUserRedacted Serialize a user without sensitive information. // BuildUserRedacted Serialize a user without sensitive information.
func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User { // The email is only exposed when the requester is a signed-in user (level >=
// RedactLevelUser) and the `expose_user_email` setting is enabled.
func BuildUserRedacted(ctx context.Context, u *ent.User, level int, idEncoder hashid.Encoder) User {
userRaw := BuildUser(u, idEncoder) userRaw := BuildUser(u, idEncoder)
user := User{ user := User{
@ -237,7 +241,7 @@ func BuildUserRedacted(u *ent.User, level int, idEncoder hashid.Encoder) User {
user.Group = RedactedGroup(userRaw.Group) user.Group = RedactedGroup(userRaw.Group)
} }
if level == RedactLevelUser { if level == RedactLevelUser && dependency.FromContext(ctx).SettingProvider().ExposeUserEmail(ctx) {
user.Email = userRaw.Email user.Email = userRaw.Email
} }

Loading…
Cancel
Save