diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md
index 8a65b522f5..2a1e3e6a6b 100644
--- a/docs/content/doc/help/faq.en-us.md
+++ b/docs/content/doc/help/faq.en-us.md
@@ -31,6 +31,7 @@ Also see [Support Options]({{< relref "doc/help/seek-help.en-us.md" >}})
   * [Only allow certain email domains](#only-allow-certain-email-domains)
   * [Only allow/block certain OpenID providers](#only-allow-block-certain-openid-providers)
   * [Issue only users](#issue-only-users)
+  * [Restricted users](#restricted-users)
   * [Enable Fail2ban](#enable-fail2ban)
 * [Adding custom themes](#how-to-add-use-custom-themes)
 * [SSHD vs built-in SSH](#sshd-vs-built-in-ssh)
@@ -147,6 +148,14 @@ You can configure `WHITELISTED_URIS` or `BLACKLISTED_URIS` under `[openid]` in y
 ### Issue only users
 The current way to achieve this is to create/modify a user with a max repo creation limit of 0.
 
+### Restricted users
+Restricted users are limited to a subset of the content based on their organization/team memberships and collaborations, ignoring the public flag on organizations/repos etc.__
+
+Example use case: A company runs a Gitea instance that requires login. Most repos are public (accessible/browseable by all co-workers).
+
+At some point, a customer or third party needs access to a specific repo and only that repo. Making such a customer account restricted and granting any needed access using team membership(s) and/or collaboration(s) is a simple way to achieve that without the need to make everything private.
+
+
 ### Enable Fail2ban
 
 Use [Fail2Ban]({{ relref "doc/usage/fail2ban-setup.md" >}}) to monitor and stop automated login attempts or other malicious behavior based on log patterns
diff --git a/models/access.go b/models/access.go
index 213efe08a6..94defbb196 100644
--- a/models/access.go
+++ b/models/access.go
@@ -71,9 +71,17 @@ type Access struct {
 	Mode   AccessMode
 }
 
-func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) {
+func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
 	mode := AccessModeNone
-	if !repo.IsPrivate {
+	var userID int64
+	restricted := false
+
+	if user != nil {
+		userID = user.ID
+		restricted = user.IsRestricted
+	}
+
+	if !restricted && !repo.IsPrivate {
 		mode = AccessModeRead
 	}
 
@@ -162,22 +170,37 @@ func maxAccessMode(modes ...AccessMode) AccessMode {
 	return max
 }
 
+type userAccess struct {
+	User *User
+	Mode AccessMode
+}
+
+// updateUserAccess updates an access map so that user has at least mode
+func updateUserAccess(accessMap map[int64]*userAccess, user *User, mode AccessMode) {
+	if ua, ok := accessMap[user.ID]; ok {
+		ua.Mode = maxAccessMode(ua.Mode, mode)
+	} else {
+		accessMap[user.ID] = &userAccess{User: user, Mode: mode}
+	}
+}
+
 // FIXME: do cross-comparison so reduce deletions and additions to the minimum?
-func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
+func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]*userAccess) (err error) {
 	minMode := AccessModeRead
 	if !repo.IsPrivate {
 		minMode = AccessModeWrite
 	}
 
 	newAccesses := make([]Access, 0, len(accessMap))
-	for userID, mode := range accessMap {
-		if mode < minMode {
+	for userID, ua := range accessMap {
+		if ua.Mode < minMode && !ua.User.IsRestricted {
 			continue
 		}
+
 		newAccesses = append(newAccesses, Access{
 			UserID: userID,
 			RepoID: repo.ID,
-			Mode:   mode,
+			Mode:   ua.Mode,
 		})
 	}
 
@@ -191,13 +214,13 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode
 }
 
 // refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
-func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
-	collaborations, err := repo.getCollaborations(e)
+func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]*userAccess) error {
+	collaborators, err := repo.getCollaborators(e)
 	if err != nil {
 		return fmt.Errorf("getCollaborations: %v", err)
 	}
-	for _, c := range collaborations {
-		accessMap[c.UserID] = c.Mode
+	for _, c := range collaborators {
+		updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
 	}
 	return nil
 }
@@ -206,7 +229,7 @@ func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int6
 // except the team whose ID is given. It is used to assign a team ID when
 // remove repository from that team.
 func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
-	accessMap := make(map[int64]AccessMode, 20)
+	accessMap := make(map[int64]*userAccess, 20)
 
 	if err = repo.getOwner(e); err != nil {
 		return err
@@ -239,7 +262,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
 			return fmt.Errorf("getMembers '%d': %v", t.ID, err)
 		}
 		for _, m := range t.Members {
-			accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
+			updateUserAccess(accessMap, m, t.Authorize)
 		}
 	}
 
@@ -300,7 +323,7 @@ func (repo *Repository) recalculateAccesses(e Engine) error {
 		return repo.recalculateTeamAccesses(e, 0)
 	}
 
-	accessMap := make(map[int64]AccessMode, 20)
+	accessMap := make(map[int64]*userAccess, 20)
 	if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
 		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
 	}
diff --git a/models/access_test.go b/models/access_test.go
index d0f0032547..103fe3a688 100644
--- a/models/access_test.go
+++ b/models/access_test.go
@@ -15,6 +15,7 @@ func TestAccessLevel(t *testing.T) {
 
 	user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 	user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
+	user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
 	// A public repository owned by User 2
 	repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
 	assert.False(t, repo1.IsPrivate)
@@ -22,6 +23,12 @@ func TestAccessLevel(t *testing.T) {
 	repo3 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
 	assert.True(t, repo3.IsPrivate)
 
+	// Another public repository
+	repo4 := AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository)
+	assert.False(t, repo4.IsPrivate)
+	// org. owned private repo
+	repo24 := AssertExistsAndLoadBean(t, &Repository{ID: 24}).(*Repository)
+
 	level, err := AccessLevel(user2, repo1)
 	assert.NoError(t, err)
 	assert.Equal(t, AccessModeOwner, level)
@@ -37,6 +44,21 @@ func TestAccessLevel(t *testing.T) {
 	level, err = AccessLevel(user5, repo3)
 	assert.NoError(t, err)
 	assert.Equal(t, AccessModeNone, level)
+
+	// restricted user has no access to a public repo
+	level, err = AccessLevel(user29, repo1)
+	assert.NoError(t, err)
+	assert.Equal(t, AccessModeNone, level)
+
+	// ... unless he's a collaborator
+	level, err = AccessLevel(user29, repo4)
+	assert.NoError(t, err)
+	assert.Equal(t, AccessModeWrite, level)
+
+	// ... or a team member
+	level, err = AccessLevel(user29, repo24)
+	assert.NoError(t, err)
+	assert.Equal(t, AccessModeRead, level)
 }
 
 func TestHasAccess(t *testing.T) {
@@ -72,6 +94,11 @@ func TestUser_GetRepositoryAccesses(t *testing.T) {
 	accesses, err := user1.GetRepositoryAccesses()
 	assert.NoError(t, err)
 	assert.Len(t, accesses, 0)
+
+	user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
+	accesses, err = user29.GetRepositoryAccesses()
+	assert.NoError(t, err)
+	assert.Len(t, accesses, 2)
 }
 
 func TestUser_GetAccessibleRepositories(t *testing.T) {
@@ -86,6 +113,11 @@ func TestUser_GetAccessibleRepositories(t *testing.T) {
 	repos, err = user2.GetAccessibleRepositories(0)
 	assert.NoError(t, err)
 	assert.Len(t, repos, 1)
+
+	user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
+	repos, err = user29.GetAccessibleRepositories(0)
+	assert.NoError(t, err)
+	assert.Len(t, repos, 2)
 }
 
 func TestRepository_RecalculateAccesses(t *testing.T) {
@@ -119,3 +151,21 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
 	assert.NoError(t, err)
 	assert.False(t, has)
 }
+
+func TestRepository_RecalculateAccesses3(t *testing.T) {
+	assert.NoError(t, PrepareTestDatabase())
+	team5 := AssertExistsAndLoadBean(t, &Team{ID: 5}).(*Team)
+	user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
+
+	has, err := x.Get(&Access{UserID: 29, RepoID: 23})
+	assert.NoError(t, err)
+	assert.False(t, has)
+
+	// adding user29 to team5 should add an explicit access row for repo 23
+	// even though repo 23 is public
+	assert.NoError(t, AddTeamMember(team5, user29.ID))
+
+	has, err = x.Get(&Access{UserID: 29, RepoID: 23})
+	assert.NoError(t, err)
+	assert.True(t, has)
+}
diff --git a/models/action.go b/models/action.go
index 1754c2a353..1a6ff75603 100644
--- a/models/action.go
+++ b/models/action.go
@@ -284,11 +284,11 @@ func (a *Action) GetIssueContent() string {
 
 // GetFeedsOptions options for retrieving feeds
 type GetFeedsOptions struct {
-	RequestedUser    *User
-	RequestingUserID int64
-	IncludePrivate   bool // include private actions
-	OnlyPerformedBy  bool // only actions performed by requested user
-	IncludeDeleted   bool // include deleted actions
+	RequestedUser   *User // the user we want activity for
+	Actor           *User // the user viewing the activity
+	IncludePrivate  bool  // include private actions
+	OnlyPerformedBy bool  // only actions performed by requested user
+	IncludeDeleted  bool  // include deleted actions
 }
 
 // GetFeeds returns actions according to the provided options
@@ -296,8 +296,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 	cond := builder.NewCond()
 
 	var repoIDs []int64
+	var actorID int64
+
+	if opts.Actor != nil {
+		actorID = opts.Actor.ID
+	}
+
 	if opts.RequestedUser.IsOrganization() {
-		env, err := opts.RequestedUser.AccessibleReposEnv(opts.RequestingUserID)
+		env, err := opts.RequestedUser.AccessibleReposEnv(actorID)
 		if err != nil {
 			return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
 		}
@@ -306,6 +312,8 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 		}
 
 		cond = cond.And(builder.In("repo_id", repoIDs))
+	} else if opts.Actor != nil {
+		cond = cond.And(builder.In("repo_id", opts.Actor.AccessibleRepoIDsQuery()))
 	}
 
 	cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
diff --git a/models/action_test.go b/models/action_test.go
index a4e224853c..ccdec8f532 100644
--- a/models/action_test.go
+++ b/models/action_test.go
@@ -33,11 +33,11 @@ func TestGetFeeds(t *testing.T) {
 	user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 
 	actions, err := GetFeeds(GetFeedsOptions{
-		RequestedUser:    user,
-		RequestingUserID: user.ID,
-		IncludePrivate:   true,
-		OnlyPerformedBy:  false,
-		IncludeDeleted:   true,
+		RequestedUser:   user,
+		Actor:           user,
+		IncludePrivate:  true,
+		OnlyPerformedBy: false,
+		IncludeDeleted:  true,
 	})
 	assert.NoError(t, err)
 	if assert.Len(t, actions, 1) {
@@ -46,10 +46,10 @@ func TestGetFeeds(t *testing.T) {
 	}
 
 	actions, err = GetFeeds(GetFeedsOptions{
-		RequestedUser:    user,
-		RequestingUserID: user.ID,
-		IncludePrivate:   false,
-		OnlyPerformedBy:  false,
+		RequestedUser:   user,
+		Actor:           user,
+		IncludePrivate:  false,
+		OnlyPerformedBy: false,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, actions, 0)
@@ -59,14 +59,14 @@ func TestGetFeeds2(t *testing.T) {
 	// test with an organization user
 	assert.NoError(t, PrepareTestDatabase())
 	org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
-	const userID = 2 // user2 is an owner of the organization
+	user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
 
 	actions, err := GetFeeds(GetFeedsOptions{
-		RequestedUser:    org,
-		RequestingUserID: userID,
-		IncludePrivate:   true,
-		OnlyPerformedBy:  false,
-		IncludeDeleted:   true,
+		RequestedUser:   org,
+		Actor:           user,
+		IncludePrivate:  true,
+		OnlyPerformedBy: false,
+		IncludeDeleted:  true,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, actions, 1)
@@ -76,11 +76,11 @@ func TestGetFeeds2(t *testing.T) {
 	}
 
 	actions, err = GetFeeds(GetFeedsOptions{
-		RequestedUser:    org,
-		RequestingUserID: userID,
-		IncludePrivate:   false,
-		OnlyPerformedBy:  false,
-		IncludeDeleted:   true,
+		RequestedUser:   org,
+		Actor:           user,
+		IncludePrivate:  false,
+		OnlyPerformedBy: false,
+		IncludeDeleted:  true,
 	})
 	assert.NoError(t, err)
 	assert.Len(t, actions, 0)
diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml
index af2c8a5293..811720c8e4 100644
--- a/models/fixtures/access.yml
+++ b/models/fixtures/access.yml
@@ -74,4 +74,16 @@
   id: 13
   user_id: 20
   repo_id: 28
-  mode: 4 # owner
\ No newline at end of file
+  mode: 4 # owner
+
+-
+  id: 14
+  user_id: 29
+  repo_id: 4
+  mode: 2 # write (collaborator)
+
+-
+  id: 15
+  user_id: 29
+  repo_id: 24
+  mode: 1 # read
diff --git a/models/fixtures/collaboration.yml b/models/fixtures/collaboration.yml
index d32e288e4c..82d46f38f0 100644
--- a/models/fixtures/collaboration.yml
+++ b/models/fixtures/collaboration.yml
@@ -14,4 +14,10 @@
   id: 3
   repo_id: 40
   user_id: 4
-  mode: 2 # write
\ No newline at end of file
+  mode: 2 # write
+
+-
+  id: 4
+  repo_id: 4
+  user_id: 29
+  mode: 2 # write
diff --git a/models/fixtures/org_user.yml b/models/fixtures/org_user.yml
index 0b6a5e60a7..a0bc4b9b43 100644
--- a/models/fixtures/org_user.yml
+++ b/models/fixtures/org_user.yml
@@ -58,3 +58,8 @@
   org_id: 6
   is_public: true
 
+-
+  id: 11
+  uid: 29
+  org_id: 17
+  is_public: true
diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml
index b7e3856172..9a8b0aff76 100644
--- a/models/fixtures/team.yml
+++ b/models/fixtures/team.yml
@@ -77,7 +77,7 @@
   name: review_team
   authorize: 1 # read
   num_repos: 1
-  num_members: 1
+  num_members: 2
 
 -
   id: 10
diff --git a/models/fixtures/team_user.yml b/models/fixtures/team_user.yml
index d541156fe8..8f21164df4 100644
--- a/models/fixtures/team_user.yml
+++ b/models/fixtures/team_user.yml
@@ -81,3 +81,9 @@
   org_id: 6
   team_id: 13
   uid: 28
+
+-
+  id: 15
+  org_id: 17
+  team_id: 9
+  uid: 29
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 09a027de79..640fd65bff 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -275,7 +275,7 @@
   avatar_email: user17@example.com
   num_repos: 2
   is_active: true
-  num_members: 2
+  num_members: 3
   num_teams: 3
 
 -
@@ -463,3 +463,18 @@
   num_following: 0
   is_active: true
 
+-
+  id: 29
+  lower_name: user29
+  name: user29
+  full_name: User 29
+  email: user29@example.com
+  passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password
+  type: 0 # individual
+  salt: ZogKvWdyEx
+  is_admin: false
+  is_restricted: true
+  avatar: avatar29
+  avatar_email: user29@example.com
+  num_repos: 0
+  is_active: true
diff --git a/models/lfs.go b/models/lfs.go
index 5f5fe2ccf4..854b715d5c 100644
--- a/models/lfs.go
+++ b/models/lfs.go
@@ -159,7 +159,7 @@ func LFSObjectAccessible(user *User, oid string) (bool, error) {
 		count, err := x.Count(&LFSMetaObject{Oid: oid})
 		return (count > 0), err
 	}
-	cond := accessibleRepositoryCondition(user.ID)
+	cond := accessibleRepositoryCondition(user)
 	count, err := x.Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Oid: oid})
 	return (count > 0), err
 }
@@ -182,7 +182,7 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error {
 	cond := builder.NewCond()
 	if !user.IsAdmin {
 		cond = builder.In("`lfs_meta_object`.repository_id",
-			builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user.ID)))
+			builder.Select("`repository`.id").From("repository").Where(accessibleRepositoryCondition(user)))
 	}
 	newMetas := make([]*LFSMetaObject, 0, len(metas))
 	if err := sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil {
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 703c168b00..6bdec1dfba 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -296,6 +296,8 @@ var migrations = []Migration{
 	NewMigration("Fix migrated repositories' git service type", fixMigratedRepositoryServiceType),
 	// v120 -> v121
 	NewMigration("Add owner_name on table repository", addOwnerNameOnRepository),
+	// v121 -> v122
+	NewMigration("add is_restricted column for users table", addIsRestricted),
 }
 
 // Migrate database to current version
diff --git a/models/migrations/v121.go b/models/migrations/v121.go
new file mode 100644
index 0000000000..c1ff7df3ad
--- /dev/null
+++ b/models/migrations/v121.go
@@ -0,0 +1,17 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import "xorm.io/xorm"
+
+func addIsRestricted(x *xorm.Engine) error {
+	// User see models/user.go
+	type User struct {
+		ID           int64 `xorm:"pk autoincr"`
+		IsRestricted bool  `xorm:"NOT NULL DEFAULT false"`
+	}
+
+	return x.Sync2(new(User))
+}
diff --git a/models/org.go b/models/org.go
index dbc71761f2..d79c0db84e 100644
--- a/models/org.go
+++ b/models/org.go
@@ -432,7 +432,7 @@ func hasOrgVisible(e Engine, org *User, user *User) bool {
 		return true
 	}
 
-	if org.Visibility == structs.VisibleTypePrivate && !org.isUserPartOfOrg(e, user.ID) {
+	if (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.isUserPartOfOrg(e, user.ID) {
 		return false
 	}
 	return true
@@ -735,7 +735,7 @@ type AccessibleReposEnvironment interface {
 
 type accessibleReposEnv struct {
 	org     *User
-	userID  int64
+	user    *User
 	teamIDs []int64
 	e       Engine
 	keyword string
@@ -749,13 +749,23 @@ func (org *User) AccessibleReposEnv(userID int64) (AccessibleReposEnvironment, e
 }
 
 func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvironment, error) {
+	var user *User
+
+	if userID > 0 {
+		u, err := getUserByID(e, userID)
+		if err != nil {
+			return nil, err
+		}
+		user = u
+	}
+
 	teamIDs, err := org.getUserTeamIDs(e, userID)
 	if err != nil {
 		return nil, err
 	}
 	return &accessibleReposEnv{
 		org:     org,
-		userID:  userID,
+		user:    user,
 		teamIDs: teamIDs,
 		e:       e,
 		orderBy: SearchOrderByRecentUpdated,
@@ -763,9 +773,12 @@ func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvi
 }
 
 func (env *accessibleReposEnv) cond() builder.Cond {
-	var cond builder.Cond = builder.Eq{
-		"`repository`.owner_id":   env.org.ID,
-		"`repository`.is_private": false,
+	var cond = builder.NewCond()
+	if env.user == nil || !env.user.IsRestricted {
+		cond = cond.Or(builder.Eq{
+			"`repository`.owner_id":   env.org.ID,
+			"`repository`.is_private": false,
+		})
 	}
 	if len(env.teamIDs) > 0 {
 		cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
diff --git a/models/repo_list.go b/models/repo_list.go
index 7b48834dba..45a506698a 100644
--- a/models/repo_list.go
+++ b/models/repo_list.go
@@ -111,8 +111,7 @@ func (repos MirrorRepositoryList) LoadAttributes() error {
 
 // SearchRepoOptions holds the search options
 type SearchRepoOptions struct {
-	UserID          int64
-	UserIsAdmin     bool
+	Actor           *User
 	Keyword         string
 	OwnerID         int64
 	PriorityOwnerID int64
@@ -180,9 +179,9 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
 	var cond = builder.NewCond()
 
 	if opts.Private {
-		if !opts.UserIsAdmin && opts.UserID != 0 && opts.UserID != opts.OwnerID {
+		if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
 			// OK we're in the context of a User
-			cond = cond.And(accessibleRepositoryCondition(opts.UserID))
+			cond = cond.And(accessibleRepositoryCondition(opts.Actor))
 		}
 	} else {
 		// Not looking at private organisations
@@ -276,6 +275,10 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
 		cond = cond.And(builder.Eq{"is_mirror": opts.Mirror == util.OptionalBoolTrue})
 	}
 
+	if opts.Actor != nil && opts.Actor.IsRestricted {
+		cond = cond.And(accessibleRepositoryCondition(opts.Actor))
+	}
+
 	if len(opts.OrderBy) == 0 {
 		opts.OrderBy = SearchOrderByAlphabetically
 	}
@@ -314,32 +317,43 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
 }
 
 // accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
-func accessibleRepositoryCondition(userID int64) builder.Cond {
-	return builder.Or(
+func accessibleRepositoryCondition(user *User) builder.Cond {
+	var cond = builder.NewCond()
+
+	if user == nil || !user.IsRestricted {
+		orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate}
+		if user == nil {
+			orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
+		}
 		// 1. Be able to see all non-private repositories that either:
-		builder.And(
+		cond = cond.Or(builder.And(
 			builder.Eq{"`repository`.is_private": false},
 			builder.Or(
 				//   A. Aren't in organisations  __OR__
 				builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"type": UserTypeOrganization})),
-				//   B. Isn't a private organisation. (Limited is OK because we're logged in)
-				builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePrivate}))),
-		),
+				//   B. Isn't a private organisation. Limited is OK as long as we're logged in.
+				builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.In("visibility", orgVisibilityLimit))))))
+	}
+
+	if user != nil {
 		// 2. Be able to see all repositories that we have access to
-		builder.Or(
+		cond = cond.Or(builder.Or(
 			builder.In("`repository`.id", builder.Select("repo_id").
 				From("`access`").
 				Where(builder.And(
-					builder.Eq{"user_id": userID},
+					builder.Eq{"user_id": user.ID},
 					builder.Gt{"mode": int(AccessModeNone)}))),
 			builder.In("`repository`.id", builder.Select("id").
 				From("`repository`").
-				Where(builder.Eq{"owner_id": userID}))),
+				Where(builder.Eq{"owner_id": user.ID}))))
 		// 3. Be able to see all repositories that we are in a team
-		builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
+		cond = cond.Or(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").
 			From("team_repo").
-			Where(builder.Eq{"`team_user`.uid": userID}).
+			Where(builder.Eq{"`team_user`.uid": user.ID}).
 			Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")))
+	}
+
+	return cond
 }
 
 // SearchRepositoryByName takes keyword and part of repository name to search,
@@ -349,25 +363,18 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
 	return SearchRepository(opts)
 }
 
+// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
+func (user *User) AccessibleRepoIDsQuery() *builder.Builder {
+	return builder.Select("id").From("repository").Where(accessibleRepositoryCondition(user))
+}
+
 // FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id
-func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) {
-	var accessCond builder.Cond = builder.Eq{"is_private": false}
-
-	if userID > 0 {
-		accessCond = accessCond.Or(
-			builder.Eq{"owner_id": userID},
-			builder.And(
-				builder.Expr("id IN (SELECT repo_id FROM `access` WHERE access.user_id = ?)", userID),
-				builder.Neq{"owner_id": userID},
-			),
-		)
-	}
-
+func FindUserAccessibleRepoIDs(user *User) ([]int64, error) {
 	repoIDs := make([]int64, 0, 10)
 	if err := x.
 		Table("repository").
 		Cols("id").
-		Where(accessCond).
+		Where(accessibleRepositoryCondition(user)).
 		Find(&repoIDs); err != nil {
 		return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err)
 	}
diff --git a/models/repo_permission.go b/models/repo_permission.go
index cd20224912..0b3e5b341a 100644
--- a/models/repo_permission.go
+++ b/models/repo_permission.go
@@ -202,7 +202,7 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
 	}
 
 	// plain user
-	perm.AccessMode, err = accessLevel(e, user.ID, repo)
+	perm.AccessMode, err = accessLevel(e, user, repo)
 	if err != nil {
 		return
 	}
@@ -250,8 +250,8 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
 			}
 		}
 
-		// for a public repo on an organization, user have read permission on non-team defined units.
-		if !found && !repo.IsPrivate {
+		// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
+		if !found && !repo.IsPrivate && !user.IsRestricted {
 			if _, ok := perm.UnitsMode[u.Type]; !ok {
 				perm.UnitsMode[u.Type] = AccessModeRead
 			}
@@ -284,7 +284,7 @@ func isUserRepoAdmin(e Engine, repo *Repository, user *User) (bool, error) {
 		return true, nil
 	}
 
-	mode, err := accessLevel(e, user.ID, repo)
+	mode, err := accessLevel(e, user, repo)
 	if err != nil {
 		return false, err
 	}
diff --git a/models/user.go b/models/user.go
index dc8ae7e0f8..ea1d110807 100644
--- a/models/user.go
+++ b/models/user.go
@@ -132,6 +132,7 @@ type User struct {
 	// Permissions
 	IsActive                bool `xorm:"INDEX"` // Activate primary email
 	IsAdmin                 bool
+	IsRestricted            bool `xorm:"NOT NULL DEFAULT false"`
 	AllowGitHook            bool
 	AllowImportLocal        bool // Allow migrate repository by local path
 	AllowCreateOrganization bool `xorm:"DEFAULT true"`
@@ -641,7 +642,7 @@ func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) {
 	if err := x.Table("repository").
 		Cols("repository.id").
 		Join("INNER", "team_user", "repository.owner_id = team_user.org_id").
-		Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true).
+		Join("INNER", "team_repo", "(? != ? and repository.is_private != ?) OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true, u.IsRestricted, true).
 		Where("team_user.uid = ?", u.ID).
 		GroupBy("repository.id").Find(&ids); err != nil {
 		return nil, err
@@ -1470,7 +1471,7 @@ type SearchUserOptions struct {
 	OrderBy       SearchOrderBy
 	Page          int
 	Visible       []structs.VisibleType
-	OwnerID       int64 // id of user for visibility calculation
+	Actor         *User // The user doing the search
 	PageSize      int   // Can be smaller than or equal to setting.UI.ExplorePagingNum
 	IsActive      util.OptionalBool
 	SearchByEmail bool // Search by email as well as username/full name
@@ -1498,7 +1499,7 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
 		cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
 	}
 
-	if opts.OwnerID > 0 {
+	if opts.Actor != nil {
 		var exprCond builder.Cond
 		if setting.Database.UseMySQL {
 			exprCond = builder.Expr("org_user.org_id = user.id")
@@ -1507,9 +1508,15 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
 		} else {
 			exprCond = builder.Expr("org_user.org_id = \"user\".id")
 		}
-		accessCond := builder.Or(
-			builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.OwnerID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))),
-			builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))
+		var accessCond = builder.NewCond()
+		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})))
+		}
 		cond = cond.And(accessCond)
 	}
 
diff --git a/models/user_heatmap_test.go b/models/user_heatmap_test.go
index f882b35247..c2825d9ff0 100644
--- a/models/user_heatmap_test.go
+++ b/models/user_heatmap_test.go
@@ -30,11 +30,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
 
 		// get the action for comparison
 		actions, err := GetFeeds(GetFeedsOptions{
-			RequestedUser:    user,
-			RequestingUserID: user.ID,
-			IncludePrivate:   true,
-			OnlyPerformedBy:  false,
-			IncludeDeleted:   true,
+			RequestedUser:   user,
+			Actor:           user,
+			IncludePrivate:  true,
+			OnlyPerformedBy: false,
+			IncludeDeleted:  true,
 		})
 		assert.NoError(t, err)
 
diff --git a/models/user_test.go b/models/user_test.go
index 95f4d5d363..2232d59963 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -153,13 +153,13 @@ func TestSearchUsers(t *testing.T) {
 	}
 
 	testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1},
-		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28})
+		[]int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29})
 
 	testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
 		[]int64{9})
 
 	testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
-		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28})
+		[]int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 28, 29})
 
 	testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
 		[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18})
diff --git a/modules/auth/admin.go b/modules/auth/admin.go
index 6e225891dd..975069a4b7 100644
--- a/modules/auth/admin.go
+++ b/modules/auth/admin.go
@@ -37,6 +37,7 @@ type AdminEditUserForm struct {
 	MaxRepoCreation         int
 	Active                  bool
 	Admin                   bool
+	Restricted              bool
 	AllowGitHook            bool
 	AllowImportLocal        bool
 	AllowCreateOrganization bool
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 4dc0b92234..38db43a57c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1751,6 +1751,7 @@ users.new_account = Create User Account
 users.name = Username
 users.activated = Activated
 users.admin = Admin
+users.restricted = Restricted
 users.repos = Repos
 users.created = Created
 users.last_login = Last Sign-In
@@ -1769,6 +1770,7 @@ users.max_repo_creation_desc = (Enter -1 to use the global default limit.)
 users.is_activated = User Account Is Activated
 users.prohibit_login = Disable Sign-In
 users.is_admin = Is Administrator
+users.is_restricted = Is Restricted
 users.allow_git_hook = May Create Git Hooks
 users.allow_import_local = May Import Local Repositories
 users.allow_create_organization = May Create Organizations
diff --git a/routers/admin/users.go b/routers/admin/users.go
index b5c7dbd383..71cda86cc2 100644
--- a/routers/admin/users.go
+++ b/routers/admin/users.go
@@ -233,6 +233,7 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
 	u.MaxRepoCreation = form.MaxRepoCreation
 	u.IsActive = form.Active
 	u.IsAdmin = form.Admin
+	u.IsRestricted = form.Restricted
 	u.AllowGitHook = form.AllowGitHook
 	u.AllowImportLocal = form.AllowImportLocal
 	u.AllowCreateOrganization = form.AllowCreateOrganization
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 69b8a36995..1219ef2e41 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -73,13 +73,12 @@ func SearchIssues(ctx *context.APIContext) {
 		AllPublic:   true,
 		TopicOnly:   false,
 		Collaborate: util.OptionalBoolNone,
-		UserIsAdmin: ctx.IsUserSiteAdmin(),
 		OrderBy:     models.SearchOrderByRecentUpdated,
+		Actor:       ctx.User,
 	}
 	if ctx.IsSigned {
 		opts.Private = true
 		opts.AllLimited = true
-		opts.UserID = ctx.User.ID
 	}
 	issueCount := 0
 	for page := 1; ; page++ {
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index c7959c6db9..9ae0c4af4e 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -126,6 +126,7 @@ func Search(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	opts := &models.SearchRepoOptions{
+		Actor:              ctx.User,
 		Keyword:            strings.Trim(ctx.Query("q"), " "),
 		OwnerID:            ctx.QueryInt64("uid"),
 		PriorityOwnerID:    ctx.QueryInt64("priority_owner_id"),
@@ -135,8 +136,6 @@ func Search(ctx *context.APIContext) {
 		Collaborate:        util.OptionalBoolNone,
 		Private:            ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
 		Template:           util.OptionalBoolNone,
-		UserIsAdmin:        ctx.IsUserSiteAdmin(),
-		UserID:             ctx.Data["SignedUserID"].(int64),
 		StarredByID:        ctx.QueryInt64("starredBy"),
 		IncludeDescription: ctx.QueryBool("includeDesc"),
 	}
diff --git a/routers/home.go b/routers/home.go
index 0f59c95705..96e13cc68f 100644
--- a/routers/home.go
+++ b/routers/home.go
@@ -72,10 +72,11 @@ func Home(ctx *context.Context) {
 
 // RepoSearchOptions when calling search repositories
 type RepoSearchOptions struct {
-	OwnerID  int64
-	Private  bool
-	PageSize int
-	TplName  base.TplName
+	OwnerID    int64
+	Private    bool
+	Restricted bool
+	PageSize   int
+	TplName    base.TplName
 }
 
 var (
@@ -136,6 +137,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
 	ctx.Data["TopicOnly"] = topicOnly
 
 	repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+		Actor:              ctx.User,
 		Page:               page,
 		PageSize:           opts.PageSize,
 		OrderBy:            orderBy,
@@ -190,6 +192,7 @@ func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplN
 	if opts.Page <= 1 {
 		opts.Page = 1
 	}
+	opts.Actor = ctx.User
 
 	var (
 		users   []*models.User
@@ -261,22 +264,16 @@ func ExploreOrganizations(ctx *context.Context) {
 	ctx.Data["PageIsExploreOrganizations"] = true
 	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
 
-	var ownerID int64
-	if ctx.User != nil && !ctx.User.IsAdmin {
-		ownerID = ctx.User.ID
+	visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
+	if ctx.User != nil {
+		visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
 	}
 
-	opts := models.SearchUserOptions{
+	RenderUserSearch(ctx, &models.SearchUserOptions{
 		Type:     models.UserTypeOrganization,
 		PageSize: setting.UI.ExplorePagingNum,
-		OwnerID:  ownerID,
-	}
-	if ctx.User != nil {
-		opts.Visible = []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate}
-	} else {
-		opts.Visible = []structs.VisibleType{structs.VisibleTypePublic}
-	}
-	RenderUserSearch(ctx, &opts, tplExploreOrganizations)
+		Visible:  visibleTypes,
+	}, tplExploreOrganizations)
 }
 
 // ExploreCode render explore code page
@@ -310,7 +307,7 @@ func ExploreCode(ctx *context.Context) {
 
 	// guest user or non-admin user
 	if ctx.User == nil || !isAdmin {
-		repoIDs, err = models.FindUserAccessibleRepoIDs(userID)
+		repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
 		if err != nil {
 			ctx.ServerError("SearchResults", err)
 			return
diff --git a/routers/org/home.go b/routers/org/home.go
index 9c24fe72fb..2f461d861b 100644
--- a/routers/org/home.go
+++ b/routers/org/home.go
@@ -80,8 +80,7 @@ func Home(ctx *context.Context) {
 		OwnerID:            org.ID,
 		OrderBy:            orderBy,
 		Private:            ctx.IsSigned,
-		UserIsAdmin:        ctx.IsUserSiteAdmin(),
-		UserID:             ctx.Data["SignedUserID"].(int64),
+		Actor:              ctx.User,
 		Page:               page,
 		IsProfile:          true,
 		PageSize:           setting.UI.User.RepoPagingNum,
diff --git a/routers/user/home.go b/routers/user/home.go
index 512c60716d..822452f1ca 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -144,6 +144,7 @@ func Dashboard(ctx *context.Context) {
 
 	retrieveFeeds(ctx, models.GetFeedsOptions{
 		RequestedUser:   ctxUser,
+		Actor:           ctx.User,
 		IncludePrivate:  true,
 		OnlyPerformedBy: false,
 		IncludeDeleted:  false,
diff --git a/routers/user/profile.go b/routers/user/profile.go
index 90e832b530..b5933788dd 100644
--- a/routers/user/profile.go
+++ b/routers/user/profile.go
@@ -161,6 +161,7 @@ func Profile(ctx *context.Context) {
 	switch tab {
 	case "activity":
 		retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
+			Actor:           ctx.User,
 			IncludePrivate:  showPrivate,
 			OnlyPerformedBy: true,
 			IncludeDeleted:  false,
@@ -171,11 +172,10 @@ func Profile(ctx *context.Context) {
 	case "stars":
 		ctx.Data["PageIsProfileStarList"] = true
 		repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+			Actor:              ctx.User,
 			Keyword:            keyword,
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
-			UserIsAdmin:        ctx.IsUserSiteAdmin(),
-			UserID:             ctx.Data["SignedUserID"].(int64),
 			Page:               page,
 			PageSize:           setting.UI.User.RepoPagingNum,
 			StarredByID:        ctxUser.ID,
@@ -191,12 +191,11 @@ func Profile(ctx *context.Context) {
 		total = int(count)
 	default:
 		repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+			Actor:              ctx.User,
 			Keyword:            keyword,
 			OwnerID:            ctxUser.ID,
 			OrderBy:            orderBy,
 			Private:            ctx.IsSigned,
-			UserIsAdmin:        ctx.IsUserSiteAdmin(),
-			UserID:             ctx.Data["SignedUserID"].(int64),
 			Page:               page,
 			IsProfile:          true,
 			PageSize:           setting.UI.User.RepoPagingNum,
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index b2ec622ca2..da75cb5065 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -83,6 +83,12 @@
 						<input name="admin" type="checkbox" {{if .User.IsAdmin}}checked{{end}}>
 					</div>
 				</div>
+				<div class="inline field">
+					<div class="ui checkbox">
+						<label><strong>{{.i18n.Tr "admin.users.is_restricted"}}</strong></label>
+						<input name="restricted" type="checkbox" {{if .User.IsRestricted}}checked{{end}}>
+					</div>
+				</div>
 				<div class="inline field">
 					<div class="ui checkbox">
 						<label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label>
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 538f9b7fed..72b7ccd191 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -21,6 +21,7 @@
 						<th>{{.i18n.Tr "email"}}</th>
 						<th>{{.i18n.Tr "admin.users.activated"}}</th>
 						<th>{{.i18n.Tr "admin.users.admin"}}</th>
+						<th>{{.i18n.Tr "admin.users.restricted"}}</th>
 						<th>{{.i18n.Tr "admin.users.repos"}}</th>
 						<th>{{.i18n.Tr "admin.users.created"}}</th>
 						<th>{{.i18n.Tr "admin.users.last_login"}}</th>
@@ -35,6 +36,7 @@
 							<td><span class="text truncate email">{{.Email}}</span></td>
 							<td><i class="fa fa{{if .IsActive}}-check{{end}}-square-o"></i></td>
 							<td><i class="fa fa{{if .IsAdmin}}-check{{end}}-square-o"></i></td>
+							<td><i class="fa fa{{if .IsRestricted}}-check{{end}}-square-o"></i></td>
 							<td>{{.NumRepos}}</td>
 							<td><span title="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</span></td>
 							{{if .LastLoginUnix}}