mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-03 21:08:25 +00:00 
			
		
		
		
	Restricted users (#6274)
* Restricted users (#4334): initial implementation
* Add User.IsRestricted & UI to edit it
* Pass user object instead of user id to places where IsRestricted flag matters
* Restricted users: maintain access rows for all referenced repos (incl public)
* Take logged in user & IsRestricted flag into account in org/repo listings, searches and accesses
* Add basic repo access tests for restricted users
Signed-off-by: Manush Dodunekov <manush@stendahls.se>
* Mention restricted users in the faq
Signed-off-by: Manush Dodunekov <manush@stendahls.se>
* Revert unnecessary change `.isUserPartOfOrg` -> `.IsUserPartOfOrg`
Signed-off-by: Manush Dodunekov <manush@stendahls.se>
* Remove unnecessary `org.IsOrganization()` call
Signed-off-by: Manush Dodunekov <manush@stendahls.se>
* Revert to an `int64` keyed `accessMap`
* Add type `userAccess`
* Add convenience func updateUserAccess()
* Turn accessMap into a `map[int64]userAccess`
Signed-off-by: Manush Dodunekov <manush@stendahls.se>
* or even better: `map[int64]*userAccess`
* updateUserAccess(): use tighter syntax as suggested by lafriks
* even tighter
* Avoid extra loop
* Don't disclose limited orgs to unauthenticated users
* Don't assume block only applies to orgs
* Use an array of `VisibleType` for filtering
* fix yet another thinko
* Ok - no need for u
* Revert "Ok - no need for u"
This reverts commit 5c3e886aab.
Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com>
Co-authored-by: Lauris BH <lauris@nix.lv>
			
			
This commit is contained in:
		
				
					committed by
					
						
						Antoine GIRARD
					
				
			
			
				
	
			
			
			
						parent
						
							0b3aaa6196
						
					
				
				
					commit
					1751d5fcf2
				
			@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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})
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -75,3 +75,15 @@
 | 
			
		||||
  user_id: 20
 | 
			
		||||
  repo_id: 28
 | 
			
		||||
  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
 | 
			
		||||
 
 | 
			
		||||
@@ -15,3 +15,9 @@
 | 
			
		||||
  repo_id: 40
 | 
			
		||||
  user_id: 4
 | 
			
		||||
  mode: 2 # write
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 4
 | 
			
		||||
  repo_id: 4
 | 
			
		||||
  user_id: 29
 | 
			
		||||
  mode: 2 # write
 | 
			
		||||
 
 | 
			
		||||
@@ -58,3 +58,8 @@
 | 
			
		||||
  org_id: 6
 | 
			
		||||
  is_public: true
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 11
 | 
			
		||||
  uid: 29
 | 
			
		||||
  org_id: 17
 | 
			
		||||
  is_public: true
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@
 | 
			
		||||
  name: review_team
 | 
			
		||||
  authorize: 1 # read
 | 
			
		||||
  num_repos: 1
 | 
			
		||||
  num_members: 1
 | 
			
		||||
  num_members: 2
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 10
 | 
			
		||||
 
 | 
			
		||||
@@ -81,3 +81,9 @@
 | 
			
		||||
  org_id: 6
 | 
			
		||||
  team_id: 13
 | 
			
		||||
  uid: 28
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 15
 | 
			
		||||
  org_id: 17
 | 
			
		||||
  team_id: 9
 | 
			
		||||
  uid: 29
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								models/migrations/v121.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								models/migrations/v121.go
									
									
									
									
									
										Normal file
									
								
							@@ -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))
 | 
			
		||||
}
 | 
			
		||||
@@ -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))
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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})
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,7 @@ type AdminEditUserForm struct {
 | 
			
		||||
	MaxRepoCreation         int
 | 
			
		||||
	Active                  bool
 | 
			
		||||
	Admin                   bool
 | 
			
		||||
	Restricted              bool
 | 
			
		||||
	AllowGitHook            bool
 | 
			
		||||
	AllowImportLocal        bool
 | 
			
		||||
	AllowCreateOrganization bool
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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++ {
 | 
			
		||||
 
 | 
			
		||||
@@ -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"),
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -144,6 +144,7 @@ func Dashboard(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	retrieveFeeds(ctx, models.GetFeedsOptions{
 | 
			
		||||
		RequestedUser:   ctxUser,
 | 
			
		||||
		Actor:           ctx.User,
 | 
			
		||||
		IncludePrivate:  true,
 | 
			
		||||
		OnlyPerformedBy: false,
 | 
			
		||||
		IncludeDeleted:  false,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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}}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user