feat(sharing): add config so that users only find members of their teams
All checks were successful
continuous-integration/drone/push Build is passing

This adds a feature where you can enable users to only find members of teams they're part of. This makes the user search when sharing projects less confusing, because users only see other users they already know.
It is still possible to add users to teams with their email address, if they have that enabled in the user settings.
This commit is contained in:
kolaente 2024-10-28 11:08:06 +01:00
parent 6a804e8b2d
commit 7055d7341c
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
7 changed files with 93 additions and 39 deletions

View File

@ -132,6 +132,11 @@
"key": "bcryptrounds",
"default_value": "11",
"comment": "The number of bcrypt rounds to use during registration. Each increment of this number doubles the computational cost. You probably don't need to change this value."
},
{
"key": "enableopenidteamusersearch",
"default_value": "false",
"comment": "If enabled, users will only find other users who are part of an existing team when they are searching for a user by their partial name. The other existing team may be created from openid. It is still possible to add users to teams with their exact email address even when this is enabled."
}
]
},

View File

@ -50,22 +50,23 @@ const (
ServiceMaxItemsPerPage Key = `service.maxitemsperpage`
ServiceDemoMode Key = `service.demomode`
// Deprecated: Use metrics.enabled
ServiceEnableMetrics Key = `service.enablemetrics`
ServiceMotd Key = `service.motd`
ServiceEnableLinkSharing Key = `service.enablelinksharing`
ServiceEnableRegistration Key = `service.enableregistration`
ServiceEnableTaskAttachments Key = `service.enabletaskattachments`
ServiceTimeZone Key = `service.timezone`
ServiceEnableTaskComments Key = `service.enabletaskcomments`
ServiceEnableTotp Key = `service.enabletotp`
ServiceTestingtoken Key = `service.testingtoken`
ServiceEnableEmailReminders Key = `service.enableemailreminders`
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
ServiceMaxAvatarSize Key = `service.maxavatarsize`
ServiceAllowIconChanges Key = `service.allowiconchanges`
ServiceCustomLogoURL Key = `service.customlogourl`
ServiceEnablePublicTeams Key = `service.enablepublicteams`
ServiceBcryptRounds Key = `service.bcryptrounds`
ServiceEnableMetrics Key = `service.enablemetrics`
ServiceMotd Key = `service.motd`
ServiceEnableLinkSharing Key = `service.enablelinksharing`
ServiceEnableRegistration Key = `service.enableregistration`
ServiceEnableTaskAttachments Key = `service.enabletaskattachments`
ServiceTimeZone Key = `service.timezone`
ServiceEnableTaskComments Key = `service.enabletaskcomments`
ServiceEnableTotp Key = `service.enabletotp`
ServiceTestingtoken Key = `service.testingtoken`
ServiceEnableEmailReminders Key = `service.enableemailreminders`
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
ServiceMaxAvatarSize Key = `service.maxavatarsize`
ServiceAllowIconChanges Key = `service.allowiconchanges`
ServiceCustomLogoURL Key = `service.customlogourl`
ServiceEnablePublicTeams Key = `service.enablepublicteams`
ServiceBcryptRounds Key = `service.bcryptrounds`
ServiceEnableOpenIDTeamUserOnlySearch Key = `service.enableopenidteamusersearch`
SentryEnabled Key = `sentry.enabled`
SentryDsn Key = `sentry.dsn`
@ -321,6 +322,7 @@ func InitDefaultConfig() {
ServiceAllowIconChanges.setDefault(true)
ServiceEnablePublicTeams.setDefault(false)
ServiceBcryptRounds.setDefault(11)
ServiceEnableOpenIDTeamUserOnlySearch.setDefault(false)
// Sentry
SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944")

View File

@ -30,7 +30,7 @@ type ProjectUIDs struct {
}
// ListUsersFromProject returns a list with all users who have access to a project, regardless of the method which gave them access
func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*user.User, err error) {
func ListUsersFromProject(s *xorm.Session, l *Project, currentUser *user.User, search string) (users []*user.User, err error) {
userids := []*ProjectUIDs{}
@ -107,7 +107,7 @@ func ListUsersFromProject(s *xorm.Session, l *Project, search string) (users []*
cond = builder.In("id", uids)
}
users, err = user.ListUsers(s, search, &user.ProjectUserOpts{
users, err = user.ListUsers(s, search, currentUser, &user.ProjectUserOpts{
AdditionalCond: cond,
ReturnAllIfNoSearchProvided: true,
MatchFuzzily: true,

View File

@ -235,7 +235,7 @@ func TestListUsersFromProject(t *testing.T) {
s := db.NewSession()
defer s.Close()
gotUsers, err := ListUsersFromProject(s, tt.args.l, tt.args.search)
gotUsers, err := ListUsersFromProject(s, tt.args.l, testuser1, tt.args.search)
if (err != nil) != tt.wantErr {
t.Errorf("ListUsersFromProject() error = %v, wantErr %v", err, tt.wantErr)
return

View File

@ -47,13 +47,14 @@ func UserList(c echo.Context) error {
s := db.NewSession()
defer s.Close()
users, err := user.ListUsers(s, search, nil)
currentUser, err := user.GetCurrentUser(c)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err)
}
if err := s.Commit(); err != nil {
users, err := user.ListUsers(s, search, currentUser, nil)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err)
}
@ -104,8 +105,14 @@ func ListUsersForProject(c echo.Context) error {
return echo.ErrForbidden
}
currentUser, err := user.GetCurrentUser(c)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err)
}
search := c.QueryParam("s")
users, err := models.ListUsersFromProject(s, &project, search)
users, err := models.ListUsersFromProject(s, &project, currentUser, search)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err)

View File

@ -373,12 +373,14 @@ func TestUpdateUserPassword(t *testing.T) {
}
func TestListUsers(t *testing.T) {
user1 := &User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user1", nil)
all, err := ListUsers(s, "user1", user1, nil)
require.NoError(t, err)
assert.NotEmpty(t, all)
assert.Equal(t, "user1", all[0].Username)
@ -388,7 +390,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "uSEr1", nil)
all, err := ListUsers(s, "uSEr1", user1, nil)
require.NoError(t, err)
assert.NotEmpty(t, all)
assert.Equal(t, "user1", all[0].Username)
@ -407,7 +409,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "", nil)
all, err := ListUsers(s, "", user1, nil)
require.NoError(t, err)
assert.Empty(t, all)
})
@ -416,7 +418,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user1@example.com", nil)
all, err := ListUsers(s, "user1@example.com", user1, nil)
require.NoError(t, err)
assert.Empty(t, all)
db.AssertExists(t, "users", map[string]interface{}{
@ -429,7 +431,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "one else", nil)
all, err := ListUsers(s, "one else", user1, nil)
require.NoError(t, err)
assert.Empty(t, all)
db.AssertExists(t, "users", map[string]interface{}{
@ -442,7 +444,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7@example.com", nil)
all, err := ListUsers(s, "user7@example.com", user1, nil)
require.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
@ -456,7 +458,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "with space", nil)
all, err := ListUsers(s, "with space", user1, nil)
require.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(12), all[0].ID)
@ -470,7 +472,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7@example.com", &ProjectUserOpts{AdditionalCond: builder.In("id", 7)})
all, err := ListUsers(s, "user7@example.com", user1, &ProjectUserOpts{AdditionalCond: builder.In("id", 7)})
require.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
@ -484,7 +486,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user7", nil)
all, err := ListUsers(s, "user7", user1, nil)
require.NoError(t, err)
assert.Len(t, all, 1)
assert.Equal(t, int64(7), all[0].ID)
@ -497,7 +499,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user", nil)
all, err := ListUsers(s, "user", user1, nil)
require.NoError(t, err)
assert.Empty(t, all)
db.AssertExists(t, "users", map[string]interface{}{
@ -509,7 +511,7 @@ func TestListUsers(t *testing.T) {
s := db.NewSession()
defer s.Close()
all, err := ListUsers(s, "user", &ProjectUserOpts{
all, err := ListUsers(s, "user", user1, &ProjectUserOpts{
MatchFuzzily: true,
})
require.NoError(t, err)

View File

@ -19,6 +19,8 @@ package user
import (
"strings"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"xorm.io/builder"
@ -33,7 +35,7 @@ type ProjectUserOpts struct {
}
// ListUsers returns a list with all users, filtered by an optional search string
func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*User, err error) {
func ListUsers(s *xorm.Session, search string, currentUser *User, opts *ProjectUserOpts) (users []*User, err error) {
if opts == nil {
opts = &ProjectUserOpts{}
}
@ -47,8 +49,10 @@ func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*
conds := []builder.Cond{}
queryParts := strings.Split(search, ",")
if search != "" {
for _, queryPart := range strings.Split(search, ",") {
for _, queryPart := range queryParts {
if opts.MatchFuzzily {
conds = append(conds,
@ -69,10 +73,6 @@ func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*
conds = append(conds,
usernameCond,
builder.And(
builder.Eq{"email": queryPart},
builder.Eq{"discoverable_by_email": true},
),
builder.And(
db.ILIKE("name", queryPart),
builder.Eq{"discoverable_by_name": true},
@ -81,6 +81,15 @@ func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*
}
}
if !opts.MatchFuzzily {
conds = append(conds,
builder.And(
builder.In("email", queryParts),
builder.Eq{"discoverable_by_email": true},
),
)
}
cond := builder.Or(conds...)
if opts.AdditionalCond != nil {
@ -90,6 +99,35 @@ func ListUsers(s *xorm.Session, search string, opts *ProjectUserOpts) (users []*
)
}
if config.ServiceEnableOpenIDTeamUserOnlySearch.GetBool() {
teamMemberCond := builder.In("id", builder.Select("user_id").
From("team_members").
Where(builder.In("team_id",
builder.Select("team_id").
From("team_members").
Where(builder.Eq{"team_members.user_id": currentUser.ID}),
)),
)
if !opts.MatchFuzzily {
cond = builder.And(
cond,
builder.Or(
teamMemberCond,
builder.And(
builder.In("email", queryParts),
builder.Eq{"discoverable_by_email": true},
),
),
)
} else {
cond = builder.And(
cond,
teamMemberCond,
)
}
}
err = s.
Where(cond).
Find(&users)