From 22a0636544237bcffb46b36b593a501e77ae02cc Mon Sep 17 00:00:00 2001
From: Sergey Dryabzhinsky <sergey@rusoft.ru>
Date: Sat, 26 Jun 2021 22:53:14 +0300
Subject: [PATCH] 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 <6543@obermui.de>
Co-authored-by: Andrew Thornton <art27@cantab.net>
---
 custom/conf/app.example.ini                   |  12 +-
 .../doc/advanced/config-cheat-sheet.en-us.md  |   1 +
 integrations/api_user_search_test.go          |  31 ++++++
 models/fixtures/user.yml                      |  18 ++-
 models/org.go                                 |  16 +--
 models/org_test.go                            |  18 +--
 models/repo.go                                |   3 +-
 models/repo_permission.go                     |   6 +-
 models/user.go                                | 105 +++++++++++++++---
 modules/convert/user.go                       |   4 +
 modules/convert/user_test.go                  |   8 ++
 modules/setting/service.go                    |   4 +
 modules/structs/admin_user.go                 |   2 +
 modules/structs/user.go                       |   2 +
 options/locale/locale_en-US.ini               |   8 ++
 routers/api/v1/admin/user.go                  |  15 ++-
 routers/api/v1/org/org.go                     |   4 +-
 routers/api/v1/repo/repo.go                   |   4 +-
 routers/api/v1/user/helper.go                 |   2 +-
 routers/api/v1/user/user.go                   |   7 ++
 routers/web/admin/orgs.go                     |   3 +-
 routers/web/admin/users.go                    |  10 +-
 routers/web/admin/users_test.go               |  81 ++++++++++++++
 routers/web/org/home.go                       |   4 +-
 routers/web/user/profile.go                   |  16 ++-
 routers/web/user/setting/profile.go           |   1 +
 services/forms/admin.go                       |   3 +
 services/forms/user_form.go                   |   2 +
 templates/admin/user/edit.tmpl                |  27 +++++
 templates/admin/user/new.tmpl                 |  19 ++++
 templates/swagger/v1_json.tmpl                |  13 +++
 templates/user/settings/profile.tmpl          |  59 ++++++++--
 32 files changed, 440 insertions(+), 68 deletions(-)

diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index fa6a9e3fac0b..e7fe9206ed96 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -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
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index aa9eb7e0caee..21359dcab144 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -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.
diff --git a/integrations/api_user_search_test.go b/integrations/api_user_search_test.go
index c5295fbba5da..f7349827e580 100644
--- a/integrations/api_user_search_test.go
+++ b/integrations/api_user_search_test.go
@@ -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)
+}
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index d903a7942f81..850ee4041d81 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -508,7 +508,6 @@
   num_repos: 0
   is_active: true
 
-
 -
   id: 30
   lower_name: user30
@@ -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
diff --git a/models/org.go b/models/org.go
index 7f9e3cce5b62..073b26c2f8b6 100644
--- a/models/org.go
+++ b/models/org.go
@@ -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
@@ -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
 		}
 	}
diff --git a/models/org_test.go b/models/org_test.go
index bed7a6eb8632..e494e502dd31 100644
--- a/models/org_test.go
+++ b/models/org_test.go
@@ -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
@@ -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
@@ -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
diff --git a/models/repo.go b/models/repo.go
index 4ce3d4839bc3..92d8427fab52 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -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.
diff --git a/models/repo_permission.go b/models/repo_permission.go
index 138613b2e92e..4f043a58ccfa 100644
--- a/models/repo_permission.go
+++ b/models/repo_permission.go
@@ -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
 	}
diff --git a/models/user.go b/models/user.go
index 599834142219..221c840a7f7f 100644
--- a/models/user.go
+++ b/models/user.go
@@ -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
@@ -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
 	}
@@ -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 {
@@ -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
@@ -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 {
@@ -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 {
diff --git a/modules/convert/user.go b/modules/convert/user.go
index 894be3bd4441..164ffb71fd2e 100644
--- a/modules/convert/user.go
+++ b/modules/convert/user.go
@@ -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
diff --git a/modules/convert/user_test.go b/modules/convert/user_test.go
index 7837910ffecb..679c4f98948a 100644
--- a/modules/convert/user_test.go
+++ b/modules/convert/user_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
+	api "code.gitea.io/gitea/modules/structs"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -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)
 }
diff --git a/modules/setting/service.go b/modules/setting/service.go
index bd70c7e6ebe5..3f689212f373 100644
--- a/modules/setting/service.go
+++ b/modules/setting/service.go
@@ -15,6 +15,8 @@ import (
 
 // Service settings
 var Service struct {
+	DefaultUserVisibility                   string
+	DefaultUserVisibilityMode               structs.VisibleType
 	DefaultOrgVisibility                    string
 	DefaultOrgVisibilityMode                structs.VisibleType
 	ActiveCodeLives                         int
@@ -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()
diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go
index 5da4e9608bea..facf16a39552 100644
--- a/modules/structs/admin_user.go
+++ b/modules/structs/admin_user.go
@@ -19,6 +19,7 @@ type CreateUserOption struct {
 	Password           string `json:"password" binding:"Required;MaxSize(255)"`
 	MustChangePassword *bool  `json:"must_change_password"`
 	SendNotify         bool   `json:"send_notify"`
+	Visibility         string `json:"visibility" binding:"In(,public,limited,private)"`
 }
 
 // EditUserOption edit user options
@@ -43,4 +44,5 @@ type EditUserOption struct {
 	ProhibitLogin           *bool   `json:"prohibit_login"`
 	AllowCreateOrganization *bool   `json:"allow_create_organization"`
 	Restricted              *bool   `json:"restricted"`
+	Visibility              string  `json:"visibility" binding:"In(,public,limited,private)"`
 }
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0d8b0300c30f..a3c8f0c32a7b 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -43,6 +43,8 @@ type User struct {
 	Website string `json:"website"`
 	// the user's description
 	Description string `json:"description"`
+	// User visibility level option: public, limited, private
+	Visibility string `json:"visibility"`
 
 	// user counts
 	Followers    int `json:"followers_count"`
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4a79ffa7eb25..e0ece8f9f0e2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -724,6 +724,14 @@ email_notifications.onmention = Only Email on Mention
 email_notifications.disable = Disable Email Notifications
 email_notifications.submit = Set Email Preference
 
+visibility = User visibility
+visibility.public = Public
+visibility.public_tooltip = Visible to all users
+visibility.limited = Limited
+visibility.limited_tooltip = Visible to logged in users only
+visibility.private = Private
+visibility.private_tooltip = Visible only to organization members
+
 [repo]
 new_repo_helper = A repository contains all project files, including revision history.  Already have it elsewhere? <a href="%s">Migrate repository.</a>
 owner = Owner
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 4bbe7f77ba2e..6bc9b849b1fc 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -66,6 +66,7 @@ func CreateUser(ctx *context.APIContext) {
 	//   "422":
 	//     "$ref": "#/responses/validationError"
 	form := web.GetForm(ctx).(*api.CreateUserOption)
+
 	u := &models.User{
 		Name:               form.Username,
 		FullName:           form.FullName,
@@ -97,7 +98,15 @@ func CreateUser(ctx *context.APIContext) {
 		ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
 		return
 	}
-	if err := models.CreateUser(u); err != nil {
+
+	var overwriteDefault *models.CreateUserOverwriteOptions
+	if form.Visibility != "" {
+		overwriteDefault = &models.CreateUserOverwriteOptions{
+			Visibility: api.VisibilityModes[form.Visibility],
+		}
+	}
+
+	if err := models.CreateUser(u, overwriteDefault); err != nil {
 		if models.IsErrUserAlreadyExist(err) ||
 			models.IsErrEmailAlreadyUsed(err) ||
 			models.IsErrNameReserved(err) ||
@@ -209,6 +218,9 @@ func EditUser(ctx *context.APIContext) {
 	if form.Active != nil {
 		u.IsActive = *form.Active
 	}
+	if len(form.Visibility) != 0 {
+		u.Visibility = api.VisibilityModes[form.Visibility]
+	}
 	if form.Admin != nil {
 		u.IsAdmin = *form.Admin
 	}
@@ -395,6 +407,7 @@ func GetAllUsers(ctx *context.APIContext) {
 	listOptions := utils.GetListOptions(ctx)
 
 	users, maxResults, err := models.SearchUsers(&models.SearchUserOptions{
+		Actor:       ctx.User,
 		Type:        models.UserTypeIndividual,
 		OrderBy:     models.SearchOrderByAlphabetically,
 		ListOptions: listOptions,
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index f4a634f4d56c..5c16594f89d1 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -225,8 +225,8 @@ func Get(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/Organization"
 
-	if !models.HasOrgVisible(ctx.Org.Organization, ctx.User) {
-		ctx.NotFound("HasOrgVisible", nil)
+	if !models.HasOrgOrUserVisible(ctx.Org.Organization, ctx.User) {
+		ctx.NotFound("HasOrgOrUserVisible", nil)
 		return
 	}
 	ctx.JSON(http.StatusOK, convert.ToOrganization(ctx.Org.Organization))
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 7a3160fa9937..35d349051057 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -375,8 +375,8 @@ func CreateOrgRepo(ctx *context.APIContext) {
 		return
 	}
 
-	if !models.HasOrgVisible(org, ctx.User) {
-		ctx.NotFound("HasOrgVisible", nil)
+	if !models.HasOrgOrUserVisible(org, ctx.User) {
+		ctx.NotFound("HasOrgOrUserVisible", nil)
 		return
 	}
 
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
index fcdac257edc8..a3500e0ee648 100644
--- a/routers/api/v1/user/helper.go
+++ b/routers/api/v1/user/helper.go
@@ -17,7 +17,7 @@ func GetUserByParamsName(ctx *context.APIContext, name string) *models.User {
 	user, err := models.GetUserByName(username)
 	if err != nil {
 		if models.IsErrUserNotExist(err) {
-			if redirectUserID, err := models.LookupUserRedirect(username); err == nil {
+			if redirectUserID, err2 := models.LookupUserRedirect(username); err2 == nil {
 				context.RedirectToUser(ctx.Context, username, redirectUserID)
 			} else {
 				ctx.NotFound("GetUserByName", err)
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 4adae532fdfc..ac543d597d7e 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -57,6 +57,7 @@ func Search(ctx *context.APIContext) {
 	listOptions := utils.GetListOptions(ctx)
 
 	opts := &models.SearchUserOptions{
+		Actor:       ctx.User,
 		Keyword:     strings.Trim(ctx.Query("q"), " "),
 		UID:         ctx.QueryInt64("uid"),
 		Type:        models.UserTypeIndividual,
@@ -102,10 +103,16 @@ func GetInfo(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	u := GetUserByParams(ctx)
+
 	if ctx.Written() {
 		return
 	}
 
+	if !u.IsVisibleToUser(ctx.User) {
+		// fake ErrUserNotExist error message to not leak information about existence
+		ctx.NotFound("GetUserByName", models.ErrUserNotExist{Name: ctx.Params(":username")})
+		return
+	}
 	ctx.JSON(http.StatusOK, convert.ToUser(u, ctx.User))
 }
 
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 618f94570449..a2b3ed1bcc0f 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -25,7 +25,8 @@ func Organizations(ctx *context.Context) {
 	ctx.Data["PageIsAdminOrganizations"] = true
 
 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{
-		Type: models.UserTypeOrganization,
+		Actor: ctx.User,
+		Type:  models.UserTypeOrganization,
 		ListOptions: models.ListOptions{
 			PageSize: setting.UI.Admin.OrgPagingNum,
 		},
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index 1b65795865fa..dc2a97e5261d 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -37,7 +37,8 @@ func Users(ctx *context.Context) {
 	ctx.Data["PageIsAdminUsers"] = true
 
 	explore.RenderUserSearch(ctx, &models.SearchUserOptions{
-		Type: models.UserTypeIndividual,
+		Actor: ctx.User,
+		Type:  models.UserTypeIndividual,
 		ListOptions: models.ListOptions{
 			PageSize: setting.UI.Admin.UserPagingNum,
 		},
@@ -50,6 +51,7 @@ func NewUser(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminUsers"] = true
+	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
 
 	ctx.Data["login_type"] = "0-0"
 
@@ -70,6 +72,7 @@ func NewUserPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("admin.users.new_account")
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminUsers"] = true
+	ctx.Data["DefaultUserVisibilityMode"] = setting.Service.DefaultUserVisibilityMode
 
 	sources, err := models.LoginSources()
 	if err != nil {
@@ -126,7 +129,8 @@ func NewUserPost(ctx *context.Context) {
 		}
 		u.MustChangePassword = form.MustChangePassword
 	}
-	if err := models.CreateUser(u); err != nil {
+
+	if err := models.CreateUser(u, &models.CreateUserOverwriteOptions{Visibility: form.Visibility}); err != nil {
 		switch {
 		case models.IsErrUserAlreadyExist(err):
 			ctx.Data["Err_UserName"] = true
@@ -312,6 +316,8 @@ func EditUserPost(ctx *context.Context) {
 	u.AllowImportLocal = form.AllowImportLocal
 	u.AllowCreateOrganization = form.AllowCreateOrganization
 
+	u.Visibility = form.Visibility
+
 	// skip self Prohibit Login
 	if ctx.User.ID == u.ID {
 		u.ProhibitLogin = false
diff --git a/routers/web/admin/users_test.go b/routers/web/admin/users_test.go
index b19dcb886bde..17c5a309b4d9 100644
--- a/routers/web/admin/users_test.go
+++ b/routers/web/admin/users_test.go
@@ -8,6 +8,8 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/setting"
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/test"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/services/forms"
@@ -121,3 +123,82 @@ func TestNewUserPost_InvalidEmail(t *testing.T) {
 
 	assert.NotEmpty(t, ctx.Flash.ErrorMsg)
 }
+
+func TestNewUserPost_VisiblityDefaultPublic(t *testing.T) {
+
+	models.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "admin/users/new")
+
+	u := models.AssertExistsAndLoadBean(t, &models.User{
+		IsAdmin: true,
+		ID:      2,
+	}).(*models.User)
+
+	ctx.User = u
+
+	username := "gitea"
+	email := "gitea@gitea.io"
+
+	form := forms.AdminCreateUserForm{
+		LoginType:          "local",
+		LoginName:          "local",
+		UserName:           username,
+		Email:              email,
+		Password:           "abc123ABC!=$",
+		SendNotify:         false,
+		MustChangePassword: false,
+	}
+
+	web.SetForm(ctx, &form)
+	NewUserPost(ctx)
+
+	assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+	u, err := models.GetUserByName(username)
+
+	assert.NoError(t, err)
+	assert.Equal(t, username, u.Name)
+	assert.Equal(t, email, u.Email)
+	// As default user visibility
+	assert.Equal(t, setting.Service.DefaultUserVisibilityMode, u.Visibility)
+}
+
+func TestNewUserPost_VisibilityPrivate(t *testing.T) {
+
+	models.PrepareTestEnv(t)
+	ctx := test.MockContext(t, "admin/users/new")
+
+	u := models.AssertExistsAndLoadBean(t, &models.User{
+		IsAdmin: true,
+		ID:      2,
+	}).(*models.User)
+
+	ctx.User = u
+
+	username := "gitea"
+	email := "gitea@gitea.io"
+
+	form := forms.AdminCreateUserForm{
+		LoginType:          "local",
+		LoginName:          "local",
+		UserName:           username,
+		Email:              email,
+		Password:           "abc123ABC!=$",
+		SendNotify:         false,
+		MustChangePassword: false,
+		Visibility:         api.VisibleTypePrivate,
+	}
+
+	web.SetForm(ctx, &form)
+	NewUserPost(ctx)
+
+	assert.NotEmpty(t, ctx.Flash.SuccessMsg)
+
+	u, err := models.GetUserByName(username)
+
+	assert.NoError(t, err)
+	assert.Equal(t, username, u.Name)
+	assert.Equal(t, email, u.Email)
+	// As default user visibility
+	assert.True(t, u.Visibility.IsPrivate())
+}
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index ad14f1845444..aad0a2a90b33 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -30,8 +30,8 @@ func Home(ctx *context.Context) {
 
 	org := ctx.Org.Organization
 
-	if !models.HasOrgVisible(org, ctx.User) {
-		ctx.NotFound("HasOrgVisible", nil)
+	if !models.HasOrgOrUserVisible(org, ctx.User) {
+		ctx.NotFound("HasOrgOrUserVisible", nil)
 		return
 	}
 
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 72d00666453e..631ca2113512 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -75,6 +75,17 @@ func Profile(ctx *context.Context) {
 		return
 	}
 
+	if ctxUser.IsOrganization() {
+		org.Home(ctx)
+		return
+	}
+
+	// check view permissions
+	if !ctxUser.IsVisibleToUser(ctx.User) {
+		ctx.NotFound("user", fmt.Errorf(uname))
+		return
+	}
+
 	// Show SSH keys.
 	if isShowKeys {
 		ShowSSHKeys(ctx, ctxUser.ID)
@@ -87,11 +98,6 @@ func Profile(ctx *context.Context) {
 		return
 	}
 
-	if ctxUser.IsOrganization() {
-		org.Home(ctx)
-		return
-	}
-
 	// Show OpenID URIs
 	openIDs, err := models.GetUserOpenIDs(ctxUser.ID)
 	if err != nil {
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index 20042caca4f7..463c4ec2038c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -114,6 +114,7 @@ func ProfilePost(ctx *context.Context) {
 	}
 	ctx.User.Description = form.Description
 	ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
+	ctx.User.Visibility = form.Visibility
 	if err := models.UpdateUserSetting(ctx.User); err != nil {
 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
 			ctx.Flash.Error(ctx.Tr("form.email_been_used"))
diff --git a/services/forms/admin.go b/services/forms/admin.go
index 2e6bbaf17201..5abef0550e39 100644
--- a/services/forms/admin.go
+++ b/services/forms/admin.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 
 	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	"gitea.com/go-chi/binding"
@@ -22,6 +23,7 @@ type AdminCreateUserForm struct {
 	Password           string `binding:"MaxSize(255)"`
 	SendNotify         bool
 	MustChangePassword bool
+	Visibility         structs.VisibleType
 }
 
 // Validate validates form fields
@@ -49,6 +51,7 @@ type AdminEditUserForm struct {
 	AllowCreateOrganization bool
 	ProhibitLogin           bool
 	Reset2FA                bool `form:"reset_2fa"`
+	Visibility              structs.VisibleType
 }
 
 // Validate validates form fields
diff --git a/services/forms/user_form.go b/services/forms/user_form.go
index 903a625da01e..439ddfc7c64c 100644
--- a/services/forms/user_form.go
+++ b/services/forms/user_form.go
@@ -12,6 +12,7 @@ import (
 
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web/middleware"
 
 	"gitea.com/go-chi/binding"
@@ -230,6 +231,7 @@ type UpdateProfileForm struct {
 	Location            string `binding:"MaxSize(50)"`
 	Language            string
 	Description         string `binding:"MaxSize(255)"`
+	Visibility          structs.VisibleType
 	KeepActivityPrivate bool
 }
 
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index af01489c0af2..dba24d9837df 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -28,6 +28,33 @@
 						</div>
 					</div>
 				</div>
+
+				<div class="inline field {{if .Err_Visibility}}error{{end}}">
+					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span>
+					<div class="ui selection type dropdown">
+						{{if .User.Visibility.IsPublic}}
+						<input type="hidden" id="visibility" name="visibility" value="0">
+						{{end}}
+						{{if .User.Visibility.IsLimited}}
+						<input type="hidden" id="visibility" name="visibility" value="1">
+						{{end}}
+						{{if .User.Visibility.IsPrivate}}
+						<input type="hidden" id="visibility" name="visibility" value="2">
+						{{end}}
+						<div class="text">
+						{{if .User.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}}
+						{{if .User.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}}
+						{{if .User.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}}
+						</div>
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="menu">
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div>
+						</div>
+					</div>
+				</div>
+
 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .User.LoginSource 0}}hide{{end}}">
 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.User.LoginName}}" autofocus>
diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl
index 885045dd0270..2e391725353a 100644
--- a/templates/admin/user/new.tmpl
+++ b/templates/admin/user/new.tmpl
@@ -24,6 +24,25 @@
 						</div>
 					</div>
 				</div>
+
+				<div class="inline field {{if .Err_Visibility}}error{{end}}">
+					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span>
+					<div class="ui selection type dropdown">
+						<input type="hidden" id="visibility" name="visibility" value="{{.visibility}}">
+						<div class="text">
+						{{if .DefaultUserVisibilityMode.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}}
+						{{if .DefaultUserVisibilityMode.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}}
+						{{if .DefaultUserVisibilityMode.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}}
+						</div>
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="menu">
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div>
+						</div>
+					</div>
+				</div>
+
 				<div class="required non-local field {{if .Err_LoginName}}error{{end}} {{if eq .login_type "0-0"}}hide{{end}}">
 					<label for="login_name">{{.i18n.Tr "admin.users.auth_login_name"}}</label>
 					<input id="login_name" name="login_name" value="{{.login_name}}">
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9453b1af32c1..7f7907b3b0c6 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -13334,6 +13334,10 @@
         "username": {
           "type": "string",
           "x-go-name": "Username"
+        },
+        "visibility": {
+          "type": "string",
+          "x-go-name": "Visibility"
         }
       },
       "x-go-package": "code.gitea.io/gitea/modules/structs"
@@ -14143,6 +14147,10 @@
           "format": "int64",
           "x-go-name": "SourceID"
         },
+        "visibility": {
+          "type": "string",
+          "x-go-name": "Visibility"
+        },
         "website": {
           "type": "string",
           "x-go-name": "Website"
@@ -16637,6 +16645,11 @@
           "format": "int64",
           "x-go-name": "StarredRepos"
         },
+        "visibility": {
+          "description": "User visibility level option: public, limited, private",
+          "type": "string",
+          "x-go-name": "Visibility"
+        },
         "website": {
           "description": "the user's website",
           "type": "string",
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 9f07226632fc..4b860049d832 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -47,27 +47,62 @@
 					<input id="location" name="location"  value="{{.SignedUser.Location}}">
 				</div>
 
-					<div class="field">
-						<label for="language">{{.i18n.Tr "settings.language"}}</label>
-						<div class="ui language selection dropdown" id="language">
-							<input name="language" type="hidden" value="{{.SignedUser.Language}}">
-							{{svg "octicon-triangle-down" 14 "dropdown icon"}}
-							<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
-							<div class="menu">
-							{{range .AllLangs}}
-								<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
-							{{end}}
-							</div>
+				<div class="field">
+					<label for="language">{{.i18n.Tr "settings.language"}}</label>
+					<div class="ui language selection dropdown" id="language">
+						<input name="language" type="hidden" value="{{.SignedUser.Language}}">
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div>
+						<div class="menu">
+						{{range .AllLangs}}
+							<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
+						{{end}}
 						</div>
 					</div>
+				</div>
+
+				<div class="ui divider"></div>
+				<!-- private block -->
+
+				<div class="field">
+					<label for="security-private"><strong>{{.i18n.Tr "settings.privacy"}}</strong></label>
+				</div>
+
+				<div class="inline field {{if .Err_Visibility}}error{{end}}">
+					<span class="inline required field"><label for="visibility">{{.i18n.Tr "settings.visibility"}}</label></span>
+					<div class="ui selection type dropdown">
+						{{if .SignedUser.Visibility.IsPublic}}
+						<input type="hidden" id="visibility" name="visibility" value="0">
+						{{end}}
+						{{if .SignedUser.Visibility.IsLimited}}
+						<input type="hidden" id="visibility" name="visibility" value="1">
+						{{end}}
+						{{if .SignedUser.Visibility.IsPrivate}}
+						<input type="hidden" id="visibility" name="visibility" value="2">
+						{{end}}
+						<div class="text">
+						{{if .SignedUser.Visibility.IsPublic}}{{.i18n.Tr "settings.visibility.public"}}{{end}}
+						{{if .SignedUser.Visibility.IsLimited}}{{.i18n.Tr "settings.visibility.limited"}}{{end}}
+						{{if .SignedUser.Visibility.IsPrivate}}{{.i18n.Tr "settings.visibility.private"}}{{end}}
+						</div>
+						{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+						<div class="menu">
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.public_tooltip"}}" data-value="0">{{.i18n.Tr "settings.visibility.public"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.limited_tooltip"}}" data-value="1">{{.i18n.Tr "settings.visibility.limited"}}</div>
+							<div class="item poping up" data-content="{{.i18n.Tr "settings.visibility.private_tooltip"}}" data-value="2">{{.i18n.Tr "settings.visibility.private"}}</div>
+						</div>
+					</div>
+				</div>
 
 				<div class="field">
-					<label for="keep-activity-private">{{.i18n.Tr "settings.privacy"}}</label>
 					<div class="ui checkbox" id="keep-activity-private">
 						<label class="poping up" data-content="{{.i18n.Tr "settings.keep_activity_private_popup"}}"><strong>{{.i18n.Tr "settings.keep_activity_private"}}</strong></label>
 						<input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}>
 					</div>
 				</div>
+
+				<div class="ui divider"></div>
+
 				<div class="field">
 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
 				</div>