Skip to content

Commit 22a0636

Browse files
sergey-dryabzhinsky6543zeripath
authored
Add Visible modes function from Organisation to Users too (#16069)
You can limit or hide organisations. This pull make it also posible for users - new strings to translte - add checkbox to user profile form - add checkbox to admin user.edit form - filter explore page user search - filter api admin and public user searches - allow admins view "hidden" users - add app option DEFAULT_USER_VISIBILITY - rewrite many files to use Visibility field - check for teams intersection - fix context output - right fake 404 if not visible Co-authored-by: 6543 <[email protected]> Co-authored-by: Andrew Thornton <[email protected]>
1 parent 19ac575 commit 22a0636

File tree

32 files changed

+440
-68
lines changed

32 files changed

+440
-68
lines changed

custom/conf/app.example.ini

+9-3
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,15 @@ PATH =
651651
;DEFAULT_ALLOW_CREATE_ORGANIZATION = true
652652
;;
653653
;; Either "public", "limited" or "private", default is "public"
654-
;; Limited is for signed user only
655-
;; Private is only for member of the organization
656-
;; Public is for everyone
654+
;; Limited is for users visible only to signed users
655+
;; Private is for users visible only to members of their organizations
656+
;; Public is for users visible for everyone
657+
;DEFAULT_USER_VISIBILITY = public
658+
;;
659+
;; Either "public", "limited" or "private", default is "public"
660+
;; Limited is for organizations visible only to signed users
661+
;; Private is for organizations visible only to members of the organization
662+
;; Public is for organizations visible to everyone
657663
;DEFAULT_ORG_VISIBILITY = public
658664
;;
659665
;; Default value for DefaultOrgMemberVisible

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+1
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ relation to port exhaustion.
512512
- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
513513
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
514514
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
515+
- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private".
515516
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
516517
- `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation.
517518
- `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea.

integrations/api_user_search_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) {
5959
}
6060
}
6161
}
62+
63+
func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
64+
defer prepareTestEnv(t)()
65+
adminUsername := "user1"
66+
session := loginUser(t, adminUsername)
67+
token := getTokenForLoggedInUser(t, session)
68+
query := "user31"
69+
req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
70+
req.SetBasicAuth(token, "x-oauth-basic")
71+
resp := session.MakeRequest(t, req, http.StatusOK)
72+
73+
var results SearchResults
74+
DecodeJSON(t, resp, &results)
75+
assert.NotEmpty(t, results.Data)
76+
for _, user := range results.Data {
77+
assert.Contains(t, user.UserName, query)
78+
assert.NotEmpty(t, user.Email)
79+
assert.EqualValues(t, "private", user.Visibility)
80+
}
81+
}
82+
83+
func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) {
84+
defer prepareTestEnv(t)()
85+
query := "user31"
86+
req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
87+
resp := MakeRequest(t, req, http.StatusOK)
88+
89+
var results SearchResults
90+
DecodeJSON(t, resp, &results)
91+
assert.Empty(t, results.Data)
92+
}

models/fixtures/user.yml

+17-1
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,6 @@
508508
num_repos: 0
509509
is_active: true
510510

511-
512511
-
513512
id: 30
514513
lower_name: user30
@@ -525,3 +524,20 @@
525524
avatar_email: [email protected]
526525
num_repos: 2
527526
is_active: true
527+
528+
-
529+
id: 31
530+
lower_name: user31
531+
name: user31
532+
full_name: "user31"
533+
534+
passwd_hash_algo: argon2
535+
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password
536+
type: 0 # individual
537+
salt: ZogKvWdyEx
538+
is_admin: false
539+
visibility: 2
540+
avatar: avatar31
541+
avatar_email: [email protected]
542+
num_repos: 0
543+
is_active: true

models/org.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
455455
Find(&orgs)
456456
}
457457

458-
// HasOrgVisible tells if the given user can see the given org
459-
func HasOrgVisible(org, user *User) bool {
460-
return hasOrgVisible(x, org, user)
458+
// HasOrgOrUserVisible tells if the given user can see the given org or user
459+
func HasOrgOrUserVisible(org, user *User) bool {
460+
return hasOrgOrUserVisible(x, org, user)
461461
}
462462

463-
func hasOrgVisible(e Engine, org, user *User) bool {
463+
func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool {
464464
// Not SignedUser
465465
if user == nil {
466-
return org.Visibility == structs.VisibleTypePublic
466+
return orgOrUser.Visibility == structs.VisibleTypePublic
467467
}
468468

469-
if user.IsAdmin {
469+
if user.IsAdmin || orgOrUser.ID == user.ID {
470470
return true
471471
}
472472

473-
if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.hasMemberWithUserID(e, user.ID) {
473+
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !orgOrUser.hasMemberWithUserID(e, user.ID) {
474474
return false
475475
}
476476
return true
@@ -483,7 +483,7 @@ func HasOrgsVisible(orgs []*User, user *User) bool {
483483
}
484484

485485
for _, org := range orgs {
486-
if HasOrgVisible(org, user) {
486+
if HasOrgOrUserVisible(org, user) {
487487
return true
488488
}
489489
}

models/org_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) {
586586
assert.NoError(t, CreateOrganization(org, owner))
587587
org = AssertExistsAndLoadBean(t,
588588
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
589-
test1 := HasOrgVisible(org, owner)
590-
test2 := HasOrgVisible(org, user3)
591-
test3 := HasOrgVisible(org, nil)
589+
test1 := HasOrgOrUserVisible(org, owner)
590+
test2 := HasOrgOrUserVisible(org, user3)
591+
test3 := HasOrgOrUserVisible(org, nil)
592592
assert.True(t, test1) // owner of org
593593
assert.True(t, test2) // user not a part of org
594594
assert.True(t, test3) // logged out user
@@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) {
609609
assert.NoError(t, CreateOrganization(org, owner))
610610
org = AssertExistsAndLoadBean(t,
611611
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
612-
test1 := HasOrgVisible(org, owner)
613-
test2 := HasOrgVisible(org, user3)
614-
test3 := HasOrgVisible(org, nil)
612+
test1 := HasOrgOrUserVisible(org, owner)
613+
test2 := HasOrgOrUserVisible(org, user3)
614+
test3 := HasOrgOrUserVisible(org, nil)
615615
assert.True(t, test1) // owner of org
616616
assert.True(t, test2) // user not a part of org
617617
assert.False(t, test3) // logged out user
@@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) {
632632
assert.NoError(t, CreateOrganization(org, owner))
633633
org = AssertExistsAndLoadBean(t,
634634
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
635-
test1 := HasOrgVisible(org, owner)
636-
test2 := HasOrgVisible(org, user3)
637-
test3 := HasOrgVisible(org, nil)
635+
test1 := HasOrgOrUserVisible(org, owner)
636+
test2 := HasOrgOrUserVisible(org, user3)
637+
test3 := HasOrgOrUserVisible(org, nil)
638638
assert.True(t, test1) // owner of org
639639
assert.False(t, test2) // user not a part of org
640640
assert.False(t, test3) // logged out user

models/repo.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User,
585585

586586
var users []*User
587587

588-
if repo.IsPrivate ||
589-
(repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) {
588+
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
590589
// This a private repository:
591590
// Anyone who can read the repository is a requestable reviewer
592591
if err := e.

models/repo_permission.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
176176
return
177177
}
178178

179-
// Prevent strangers from checking out public repo of private orginization
180-
// Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself
181-
if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator {
179+
// Prevent strangers from checking out public repo of private orginization/users
180+
// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
181+
if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator {
182182
perm.AccessMode = AccessModeNone
183183
return
184184
}

models/user.go

+91-14
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool {
432432
return len(u.Passwd) != 0
433433
}
434434

435+
// IsVisibleToUser check if viewer is able to see user profile
436+
func (u *User) IsVisibleToUser(viewer *User) bool {
437+
return u.isVisibleToUser(x, viewer)
438+
}
439+
440+
func (u *User) isVisibleToUser(e Engine, viewer *User) bool {
441+
if viewer != nil && viewer.IsAdmin {
442+
return true
443+
}
444+
445+
switch u.Visibility {
446+
case structs.VisibleTypePublic:
447+
return true
448+
case structs.VisibleTypeLimited:
449+
if viewer == nil || viewer.IsRestricted {
450+
return false
451+
}
452+
return true
453+
case structs.VisibleTypePrivate:
454+
if viewer == nil || viewer.IsRestricted {
455+
return false
456+
}
457+
458+
// If they follow - they see each over
459+
follower := IsFollowing(u.ID, viewer.ID)
460+
if follower {
461+
return true
462+
}
463+
464+
// Now we need to check if they in some organization together
465+
count, err := x.Table("team_user").
466+
Where(
467+
builder.And(
468+
builder.Eq{"uid": viewer.ID},
469+
builder.Or(
470+
builder.Eq{"org_id": u.ID},
471+
builder.In("org_id",
472+
builder.Select("org_id").
473+
From("team_user", "t2").
474+
Where(builder.Eq{"uid": u.ID}))))).
475+
Count(new(TeamUser))
476+
if err != nil {
477+
return false
478+
}
479+
480+
if count < 0 {
481+
// No common organization
482+
return false
483+
}
484+
485+
// they are in an organization together
486+
return true
487+
}
488+
return false
489+
}
490+
435491
// IsOrganization returns true if user is actually a organization.
436492
func (u *User) IsOrganization() bool {
437493
return u.Type == UserTypeOrganization
@@ -796,8 +852,13 @@ func IsUsableUsername(name string) error {
796852
return isUsableName(reservedUsernames, reservedUserPatterns, name)
797853
}
798854

855+
// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
856+
type CreateUserOverwriteOptions struct {
857+
Visibility structs.VisibleType
858+
}
859+
799860
// CreateUser creates record of a new user.
800-
func CreateUser(u *User) (err error) {
861+
func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
801862
if err = IsUsableUsername(u.Name); err != nil {
802863
return err
803864
}
@@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) {
831892
return ErrEmailAlreadyUsed{u.Email}
832893
}
833894

834-
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
835-
836895
u.LowerName = strings.ToLower(u.Name)
837896
u.AvatarEmail = u.Email
838897
if u.Rands, err = GetUserSalt(); err != nil {
@@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) {
841900
if err = u.SetPassword(u.Passwd); err != nil {
842901
return err
843902
}
903+
904+
// set system defaults
905+
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
906+
u.Visibility = setting.Service.DefaultUserVisibilityMode
844907
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
845908
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
846909
u.MaxRepoCreation = -1
847910
u.Theme = setting.UI.DefaultTheme
911+
// overwrite defaults if set
912+
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
913+
u.Visibility = overwriteDefault[0].Visibility
914+
}
848915

849916
if _, err = sess.Insert(u); err != nil {
850917
return err
@@ -1527,10 +1594,9 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
15271594
cond = cond.And(keywordCond)
15281595
}
15291596

1597+
// If visibility filtered
15301598
if len(opts.Visible) > 0 {
15311599
cond = cond.And(builder.In("visibility", opts.Visible))
1532-
} else {
1533-
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
15341600
}
15351601

15361602
if opts.Actor != nil {
@@ -1543,16 +1609,27 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
15431609
exprCond = builder.Expr("org_user.org_id = \"user\".id")
15441610
}
15451611

1546-
var accessCond builder.Cond
1547-
if !opts.Actor.IsRestricted {
1548-
accessCond = builder.Or(
1549-
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
1550-
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
1551-
} else {
1552-
// restricted users only see orgs they are a member of
1553-
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
1612+
// If Admin - they see all users!
1613+
if !opts.Actor.IsAdmin {
1614+
// Force visiblity for privacy
1615+
var accessCond builder.Cond
1616+
if !opts.Actor.IsRestricted {
1617+
accessCond = builder.Or(
1618+
builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
1619+
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
1620+
} else {
1621+
// restricted users only see orgs they are a member of
1622+
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
1623+
}
1624+
// Don't forget about self
1625+
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
1626+
cond = cond.And(accessCond)
15541627
}
1555-
cond = cond.And(accessCond)
1628+
1629+
} else {
1630+
// Force visiblity for privacy
1631+
// Not logged in - only public users
1632+
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
15561633
}
15571634

15581635
if opts.UID > 0 {

modules/convert/user.go

+4
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User {
6262
Following: user.NumFollowing,
6363
StarredRepos: user.NumStars,
6464
}
65+
66+
result.Visibility = user.Visibility.String()
67+
6568
// hide primary email if API caller is anonymous or user keep email private
6669
if signed && (!user.KeepEmailPrivate || authed) {
6770
result.Email = user.Email
6871
}
72+
6973
// only site admin will get these information and possibly user himself
7074
if authed {
7175
result.IsAdmin = user.IsAdmin

modules/convert/user_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"code.gitea.io/gitea/models"
11+
api "code.gitea.io/gitea/modules/structs"
1112

1213
"github.com/stretchr/testify/assert"
1314
)
@@ -27,4 +28,11 @@ func TestUser_ToUser(t *testing.T) {
2728

2829
apiUser = toUser(user1, false, false)
2930
assert.False(t, apiUser.IsAdmin)
31+
assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)
32+
33+
user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User)
34+
35+
apiUser = toUser(user31, true, true)
36+
assert.False(t, apiUser.IsAdmin)
37+
assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
3038
}

modules/setting/service.go

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515

1616
// Service settings
1717
var Service struct {
18+
DefaultUserVisibility string
19+
DefaultUserVisibilityMode structs.VisibleType
1820
DefaultOrgVisibility string
1921
DefaultOrgVisibilityMode structs.VisibleType
2022
ActiveCodeLives int
@@ -118,6 +120,8 @@ func newService() {
118120
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
119121
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
120122
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false)
123+
Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
124+
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility]
121125
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
122126
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
123127
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()

0 commit comments

Comments
 (0)