Skip to content

Commit

Permalink
Add Visible modes function from Organisation to Users too (#16069)
Browse files Browse the repository at this point in the history
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 <6543@obermui.de>
Co-authored-by: Andrew Thornton <art27@cantab.net>
  • Loading branch information
3 people authored Jun 26, 2021
1 parent 19ac575 commit 22a0636
Show file tree
Hide file tree
Showing 32 changed files with 440 additions and 68 deletions.
12 changes: 9 additions & 3 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -651,9 +651,15 @@ PATH =
;DEFAULT_ALLOW_CREATE_ORGANIZATION = true
;;
;; Either "public", "limited" or "private", default is "public"
;; Limited is for signed user only
;; Private is only for member of the organization
;; Public is for everyone
;; Limited is for users visible only to signed users
;; Private is for users visible only to members of their organizations
;; Public is for users visible for everyone
;DEFAULT_USER_VISIBILITY = public
;;
;; Either "public", "limited" or "private", default is "public"
;; Limited is for organizations visible only to signed users
;; Private is for organizations visible only to members of the organization
;; Public is for organizations visible to everyone
;DEFAULT_ORG_VISIBILITY = public
;;
;; Default value for DefaultOrgMemberVisible
Expand Down
1 change: 1 addition & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ relation to port exhaustion.
- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
- `DEFAULT_USER_VISIBILITY`: **public**: Set default visibility mode for users, either "public", "limited" or "private".
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
- `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation.
- `ALLOW_ONLY_INTERNAL_REGISTRATION`: **false** Set to true to force registration only via gitea.
Expand Down
31 changes: 31 additions & 0 deletions integrations/api_user_search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,34 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) {
}
}
}

func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
defer prepareTestEnv(t)()
adminUsername := "user1"
session := loginUser(t, adminUsername)
token := getTokenForLoggedInUser(t, session)
query := "user31"
req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
req.SetBasicAuth(token, "x-oauth-basic")
resp := session.MakeRequest(t, req, http.StatusOK)

var results SearchResults
DecodeJSON(t, resp, &results)
assert.NotEmpty(t, results.Data)
for _, user := range results.Data {
assert.Contains(t, user.UserName, query)
assert.NotEmpty(t, user.Email)
assert.EqualValues(t, "private", user.Visibility)
}
}

func TestAPIUserSearchNotLoggedInUserHidden(t *testing.T) {
defer prepareTestEnv(t)()
query := "user31"
req := NewRequestf(t, "GET", "/api/v1/users/search?q=%s", query)
resp := MakeRequest(t, req, http.StatusOK)

var results SearchResults
DecodeJSON(t, resp, &results)
assert.Empty(t, results.Data)
}
18 changes: 17 additions & 1 deletion models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@
num_repos: 0
is_active: true


-
id: 30
lower_name: user30
Expand All @@ -525,3 +524,20 @@
avatar_email: user30@example.com
num_repos: 2
is_active: true

-
id: 31
lower_name: user31
name: user31
full_name: "user31"
email: user31@example.com
passwd_hash_algo: argon2
passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b # password
type: 0 # individual
salt: ZogKvWdyEx
is_admin: false
visibility: 2
avatar: avatar31
avatar_email: user31@example.com
num_repos: 0
is_active: true
16 changes: 8 additions & 8 deletions models/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,22 +455,22 @@ func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
Find(&orgs)
}

// HasOrgVisible tells if the given user can see the given org
func HasOrgVisible(org, user *User) bool {
return hasOrgVisible(x, org, user)
// HasOrgOrUserVisible tells if the given user can see the given org or user
func HasOrgOrUserVisible(org, user *User) bool {
return hasOrgOrUserVisible(x, org, user)
}

func hasOrgVisible(e Engine, org, user *User) bool {
func hasOrgOrUserVisible(e Engine, orgOrUser, user *User) bool {
// Not SignedUser
if user == nil {
return org.Visibility == structs.VisibleTypePublic
return orgOrUser.Visibility == structs.VisibleTypePublic
}

if user.IsAdmin {
if user.IsAdmin || orgOrUser.ID == user.ID {
return true
}

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

for _, org := range orgs {
if HasOrgVisible(org, user) {
if HasOrgOrUserVisible(org, user) {
return true
}
}
Expand Down
18 changes: 9 additions & 9 deletions models/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,9 +586,9 @@ func TestHasOrgVisibleTypePublic(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
test1 := HasOrgVisible(org, owner)
test2 := HasOrgVisible(org, user3)
test3 := HasOrgVisible(org, nil)
test1 := HasOrgOrUserVisible(org, owner)
test2 := HasOrgOrUserVisible(org, user3)
test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.True(t, test3) // logged out user
Expand All @@ -609,9 +609,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
test1 := HasOrgVisible(org, owner)
test2 := HasOrgVisible(org, user3)
test3 := HasOrgVisible(org, nil)
test1 := HasOrgOrUserVisible(org, owner)
test2 := HasOrgOrUserVisible(org, user3)
test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
Expand All @@ -632,9 +632,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) {
assert.NoError(t, CreateOrganization(org, owner))
org = AssertExistsAndLoadBean(t,
&User{Name: org.Name, Type: UserTypeOrganization}).(*User)
test1 := HasOrgVisible(org, owner)
test2 := HasOrgVisible(org, user3)
test3 := HasOrgVisible(org, nil)
test1 := HasOrgOrUserVisible(org, owner)
test2 := HasOrgOrUserVisible(org, user3)
test3 := HasOrgOrUserVisible(org, nil)
assert.True(t, test1) // owner of org
assert.False(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
Expand Down
3 changes: 1 addition & 2 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,7 @@ func (repo *Repository) getReviewers(e Engine, doerID, posterID int64) ([]*User,

var users []*User

if repo.IsPrivate ||
(repo.Owner.IsOrganization() && repo.Owner.Visibility == api.VisibleTypePrivate) {
if repo.IsPrivate || repo.Owner.Visibility == api.VisibleTypePrivate {
// This a private repository:
// Anyone who can read the repository is a requestable reviewer
if err := e.
Expand Down
6 changes: 3 additions & 3 deletions models/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,9 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
return
}

// Prevent strangers from checking out public repo of private orginization
// Allow user if they are collaborator of a repo within a private orginization but not a member of the orginization itself
if repo.Owner.IsOrganization() && !hasOrgVisible(e, repo.Owner, user) && !isCollaborator {
// Prevent strangers from checking out public repo of private orginization/users
// 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
if !hasOrgOrUserVisible(e, repo.Owner, user) && !isCollaborator {
perm.AccessMode = AccessModeNone
return
}
Expand Down
105 changes: 91 additions & 14 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,62 @@ func (u *User) IsPasswordSet() bool {
return len(u.Passwd) != 0
}

// IsVisibleToUser check if viewer is able to see user profile
func (u *User) IsVisibleToUser(viewer *User) bool {
return u.isVisibleToUser(x, viewer)
}

func (u *User) isVisibleToUser(e Engine, viewer *User) bool {
if viewer != nil && viewer.IsAdmin {
return true
}

switch u.Visibility {
case structs.VisibleTypePublic:
return true
case structs.VisibleTypeLimited:
if viewer == nil || viewer.IsRestricted {
return false
}
return true
case structs.VisibleTypePrivate:
if viewer == nil || viewer.IsRestricted {
return false
}

// If they follow - they see each over
follower := IsFollowing(u.ID, viewer.ID)
if follower {
return true
}

// Now we need to check if they in some organization together
count, err := x.Table("team_user").
Where(
builder.And(
builder.Eq{"uid": viewer.ID},
builder.Or(
builder.Eq{"org_id": u.ID},
builder.In("org_id",
builder.Select("org_id").
From("team_user", "t2").
Where(builder.Eq{"uid": u.ID}))))).
Count(new(TeamUser))
if err != nil {
return false
}

if count < 0 {
// No common organization
return false
}

// they are in an organization together
return true
}
return false
}

// IsOrganization returns true if user is actually a organization.
func (u *User) IsOrganization() bool {
return u.Type == UserTypeOrganization
Expand Down Expand Up @@ -796,8 +852,13 @@ func IsUsableUsername(name string) error {
return isUsableName(reservedUsernames, reservedUserPatterns, name)
}

// CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation
type CreateUserOverwriteOptions struct {
Visibility structs.VisibleType
}

// CreateUser creates record of a new user.
func CreateUser(u *User) (err error) {
func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err error) {
if err = IsUsableUsername(u.Name); err != nil {
return err
}
Expand Down Expand Up @@ -831,8 +892,6 @@ func CreateUser(u *User) (err error) {
return ErrEmailAlreadyUsed{u.Email}
}

u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate

u.LowerName = strings.ToLower(u.Name)
u.AvatarEmail = u.Email
if u.Rands, err = GetUserSalt(); err != nil {
Expand All @@ -841,10 +900,18 @@ func CreateUser(u *User) (err error) {
if err = u.SetPassword(u.Passwd); err != nil {
return err
}

// set system defaults
u.KeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
u.Visibility = setting.Service.DefaultUserVisibilityMode
u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation
u.EmailNotificationsPreference = setting.Admin.DefaultEmailNotification
u.MaxRepoCreation = -1
u.Theme = setting.UI.DefaultTheme
// overwrite defaults if set
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
u.Visibility = overwriteDefault[0].Visibility
}

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

// If visibility filtered
if len(opts.Visible) > 0 {
cond = cond.And(builder.In("visibility", opts.Visible))
} else {
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
}

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

var accessCond builder.Cond
if !opts.Actor.IsRestricted {
accessCond = builder.Or(
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}))),
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
} else {
// restricted users only see orgs they are a member of
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
// If Admin - they see all users!
if !opts.Actor.IsAdmin {
// Force visiblity for privacy
var accessCond builder.Cond
if !opts.Actor.IsRestricted {
accessCond = builder.Or(
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}))),
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
} else {
// restricted users only see orgs they are a member of
accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID})))
}
// Don't forget about self
accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID})
cond = cond.And(accessCond)
}
cond = cond.And(accessCond)

} else {
// Force visiblity for privacy
// Not logged in - only public users
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
}

if opts.UID > 0 {
Expand Down
4 changes: 4 additions & 0 deletions modules/convert/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ func toUser(user *models.User, signed, authed bool) *api.User {
Following: user.NumFollowing,
StarredRepos: user.NumStars,
}

result.Visibility = user.Visibility.String()

// hide primary email if API caller is anonymous or user keep email private
if signed && (!user.KeepEmailPrivate || authed) {
result.Email = user.Email
}

// only site admin will get these information and possibly user himself
if authed {
result.IsAdmin = user.IsAdmin
Expand Down
8 changes: 8 additions & 0 deletions modules/convert/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"

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

apiUser = toUser(user1, false, false)
assert.False(t, apiUser.IsAdmin)
assert.EqualValues(t, api.VisibleTypePublic.String(), apiUser.Visibility)

user31 := models.AssertExistsAndLoadBean(t, &models.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate}).(*models.User)

apiUser = toUser(user31, true, true)
assert.False(t, apiUser.IsAdmin)
assert.EqualValues(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
}
4 changes: 4 additions & 0 deletions modules/setting/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (

// Service settings
var Service struct {
DefaultUserVisibility string
DefaultUserVisibilityMode structs.VisibleType
DefaultOrgVisibility string
DefaultOrgVisibilityMode structs.VisibleType
ActiveCodeLives int
Expand Down Expand Up @@ -118,6 +120,8 @@ func newService() {
Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true)
Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true)
Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false)
Service.DefaultUserVisibility = sec.Key("DEFAULT_USER_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
Service.DefaultUserVisibilityMode = structs.VisibilityModes[Service.DefaultUserVisibility]
Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes))
Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility]
Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool()
Expand Down
Loading

0 comments on commit 22a0636

Please sign in to comment.