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>
This commit is contained in:
Manush Dodunekov 2019-04-15 05:22:14 +02:00
parent 5749b26cdd
commit 836f9d86aa
30 changed files with 281 additions and 117 deletions

View file

@ -71,9 +71,17 @@ type Access struct {
Mode AccessMode Mode AccessMode
} }
func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) { func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
mode := AccessModeNone 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 mode = AccessModeRead
} }
@ -163,17 +171,23 @@ func maxAccessMode(modes ...AccessMode) AccessMode {
} }
// FIXME: do cross-comparison so reduce deletions and additions to the minimum? // 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[*User]AccessMode) (err error) {
minMode := AccessModeRead minMode := AccessModeRead
if !repo.IsPrivate { if !repo.IsPrivate {
minMode = AccessModeWrite minMode = AccessModeWrite
} }
newAccesses := make([]Access, 0, len(accessMap)) // merge accessMap contents into a single ID-keyed map to eliminate duplicates
for userID, mode := range accessMap { idMap := make(map[int64]AccessMode, len(accessMap))
if mode < minMode { for user, mode := range accessMap {
if mode < minMode && !user.IsRestricted {
continue continue
} }
idMap[user.ID] = maxAccessMode(idMap[user.ID], mode)
}
newAccesses := make([]Access, 0, len(idMap))
for userID, mode := range idMap {
newAccesses = append(newAccesses, Access{ newAccesses = append(newAccesses, Access{
UserID: userID, UserID: userID,
RepoID: repo.ID, RepoID: repo.ID,
@ -191,13 +205,13 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode
} }
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes. // refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error { func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[*User]AccessMode) error {
collaborations, err := repo.getCollaborations(e) collaborators, err := repo.getCollaborators(e)
if err != nil { if err != nil {
return fmt.Errorf("getCollaborations: %v", err) return fmt.Errorf("getCollaborations: %v", err)
} }
for _, c := range collaborations { for _, c := range collaborators {
accessMap[c.UserID] = c.Mode accessMap[c.User] = maxAccessMode(accessMap[c.User], c.Collaboration.Mode)
} }
return nil return nil
} }
@ -206,7 +220,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 // except the team whose ID is given. It is used to assign a team ID when
// remove repository from that team. // remove repository from that team.
func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) { func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
accessMap := make(map[int64]AccessMode, 20) accessMap := make(map[*User]AccessMode, 20)
if err = repo.getOwner(e); err != nil { if err = repo.getOwner(e); err != nil {
return err return err
@ -239,7 +253,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
return fmt.Errorf("getMembers '%d': %v", t.ID, err) return fmt.Errorf("getMembers '%d': %v", t.ID, err)
} }
for _, m := range t.Members { for _, m := range t.Members {
accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize) accessMap[m] = maxAccessMode(accessMap[m], t.Authorize)
} }
} }
@ -300,7 +314,7 @@ func (repo *Repository) recalculateAccesses(e Engine) error {
return repo.recalculateTeamAccesses(e, 0) return repo.recalculateTeamAccesses(e, 0)
} }
accessMap := make(map[int64]AccessMode, 20) accessMap := make(map[*User]AccessMode, 20)
if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil { if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
return fmt.Errorf("refreshCollaboratorAccesses: %v", err) return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
} }

View file

@ -15,6 +15,7 @@ func TestAccessLevel(t *testing.T) {
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
// A public repository owned by User 2 // A public repository owned by User 2
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.False(t, repo1.IsPrivate) assert.False(t, repo1.IsPrivate)
@ -22,6 +23,12 @@ func TestAccessLevel(t *testing.T) {
repo3 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) repo3 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
assert.True(t, repo3.IsPrivate) 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) level, err := AccessLevel(user2, repo1)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, AccessModeOwner, level) assert.Equal(t, AccessModeOwner, level)
@ -37,6 +44,21 @@ func TestAccessLevel(t *testing.T) {
level, err = AccessLevel(user5, repo3) level, err = AccessLevel(user5, repo3)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, AccessModeNone, level) 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) { func TestHasAccess(t *testing.T) {
@ -72,6 +94,11 @@ func TestUser_GetRepositoryAccesses(t *testing.T) {
accesses, err := user1.GetRepositoryAccesses() accesses, err := user1.GetRepositoryAccesses()
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, accesses, 0) 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) { func TestUser_GetAccessibleRepositories(t *testing.T) {
@ -86,6 +113,11 @@ func TestUser_GetAccessibleRepositories(t *testing.T) {
repos, err = user2.GetAccessibleRepositories(0) repos, err = user2.GetAccessibleRepositories(0)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, repos, 1) 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) { func TestRepository_RecalculateAccesses(t *testing.T) {
@ -119,3 +151,21 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, has) 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)
}

View file

@ -410,11 +410,11 @@ func (pc *PushCommits) AvatarLink(email string) string {
// GetFeedsOptions options for retrieving feeds // GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct { type GetFeedsOptions struct {
RequestedUser *User RequestedUser *User // the user we want activity for
RequestingUserID int64 Actor *User // the user viewing the activity
IncludePrivate bool // include private actions IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions IncludeDeleted bool // include deleted actions
} }
// GetFeeds returns actions according to the provided options // GetFeeds returns actions according to the provided options
@ -422,8 +422,14 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
cond := builder.NewCond() cond := builder.NewCond()
var repoIDs []int64 var repoIDs []int64
var actorID int64
if opts.Actor != nil {
actorID = opts.Actor.ID
}
if opts.RequestedUser.IsOrganization() { if opts.RequestedUser.IsOrganization() {
env, err := opts.RequestedUser.AccessibleReposEnv(opts.RequestingUserID) env, err := opts.RequestedUser.AccessibleReposEnv(actorID)
if err != nil { if err != nil {
return nil, fmt.Errorf("AccessibleReposEnv: %v", err) return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
} }
@ -432,6 +438,8 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
} }
cond = cond.And(builder.In("repo_id", repoIDs)) 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}) cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})

View file

@ -133,11 +133,11 @@ func TestGetFeeds(t *testing.T) {
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
actions, err := GetFeeds(GetFeedsOptions{ actions, err := GetFeeds(GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
RequestingUserID: user.ID, Actor: user,
IncludePrivate: true, IncludePrivate: true,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: true, IncludeDeleted: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
if assert.Len(t, actions, 1) { if assert.Len(t, actions, 1) {
@ -146,10 +146,10 @@ func TestGetFeeds(t *testing.T) {
} }
actions, err = GetFeeds(GetFeedsOptions{ actions, err = GetFeeds(GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
RequestingUserID: user.ID, Actor: user,
IncludePrivate: false, IncludePrivate: false,
OnlyPerformedBy: false, OnlyPerformedBy: false,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)
@ -159,14 +159,14 @@ func TestGetFeeds2(t *testing.T) {
// test with an organization user // test with an organization user
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User) 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{ actions, err := GetFeeds(GetFeedsOptions{
RequestedUser: org, RequestedUser: org,
RequestingUserID: userID, Actor: user,
IncludePrivate: true, IncludePrivate: true,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: true, IncludeDeleted: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 1) assert.Len(t, actions, 1)
@ -176,11 +176,11 @@ func TestGetFeeds2(t *testing.T) {
} }
actions, err = GetFeeds(GetFeedsOptions{ actions, err = GetFeeds(GetFeedsOptions{
RequestedUser: org, RequestedUser: org,
RequestingUserID: userID, Actor: user,
IncludePrivate: false, IncludePrivate: false,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: true, IncludeDeleted: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, actions, 0) assert.Len(t, actions, 0)

View file

@ -75,3 +75,15 @@
user_id: 20 user_id: 20
repo_id: 28 repo_id: 28
mode: 4 # owner 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

View file

@ -15,3 +15,9 @@
repo_id: 40 repo_id: 40
user_id: 4 user_id: 4
mode: 2 # write mode: 2 # write
-
id: 4
repo_id: 4
user_id: 29
mode: 2 # write

View file

@ -58,3 +58,8 @@
org_id: 6 org_id: 6
is_public: true is_public: true
-
id: 11
uid: 29
org_id: 17
is_public: true

View file

@ -77,7 +77,7 @@
name: review_team name: review_team
authorize: 1 # read authorize: 1 # read
num_repos: 1 num_repos: 1
num_members: 1 num_members: 2
- -
id: 10 id: 10

View file

@ -81,3 +81,9 @@
org_id: 6 org_id: 6
team_id: 13 team_id: 13
uid: 28 uid: 28
-
id: 15
org_id: 17
team_id: 9
uid: 29

View file

@ -275,7 +275,7 @@
avatar_email: user17@example.com avatar_email: user17@example.com
num_repos: 2 num_repos: 2
is_active: true is_active: true
num_members: 2 num_members: 3
num_teams: 3 num_teams: 3
- -
@ -463,3 +463,18 @@
num_following: 0 num_following: 0
is_active: true 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

View file

@ -159,7 +159,7 @@ func LFSObjectAccessible(user *User, oid string) (bool, error) {
count, err := x.Count(&LFSMetaObject{Oid: oid}) count, err := x.Count(&LFSMetaObject{Oid: oid})
return (count > 0), err 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}) count, err := x.Where(cond).Join("INNER", "repository", "`lfs_meta_object`.repository_id = `repository`.id").Count(&LFSMetaObject{Oid: oid})
return (count > 0), err return (count > 0), err
} }
@ -182,7 +182,7 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *User, repoID int64) error {
cond := builder.NewCond() cond := builder.NewCond()
if !user.IsAdmin { if !user.IsAdmin {
cond = builder.In("`lfs_meta_object`.repository_id", 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)) newMetas := make([]*LFSMetaObject, 0, len(metas))
if err := sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil { if err := sess.Cols("oid").Where(cond).In("oid", oids...).GroupBy("oid").Find(&newMetas); err != nil {

View file

@ -290,6 +290,8 @@ var migrations = []Migration{
NewMigration("Extend TrackedTimes", extendTrackedTimes), NewMigration("Extend TrackedTimes", extendTrackedTimes),
// v117 -> v118 // v117 -> v118
NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews), NewMigration("Add block on rejected reviews branch protection", addBlockOnRejectedReviews),
// v118 -> v119
NewMigration("add is_restricted column for users table", addIsRestricted),
} }
// Migrate database to current version // Migrate database to current version

17
models/migrations/v118.go Normal file
View file

@ -0,0 +1,17 @@
// Copyright 2019 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))
}

View file

@ -432,7 +432,7 @@ func hasOrgVisible(e Engine, org *User, user *User) bool {
return true return true
} }
if org.Visibility == structs.VisibleTypePrivate && !org.isUserPartOfOrg(e, user.ID) { if org.IsOrganization() && (org.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !org.IsUserPartOfOrg(user.ID) {
return false return false
} }
return true return true
@ -735,7 +735,7 @@ type AccessibleReposEnvironment interface {
type accessibleReposEnv struct { type accessibleReposEnv struct {
org *User org *User
userID int64 user *User
teamIDs []int64 teamIDs []int64
e Engine e Engine
keyword string keyword string
@ -749,13 +749,23 @@ func (org *User) AccessibleReposEnv(userID int64) (AccessibleReposEnvironment, e
} }
func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvironment, error) { 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) teamIDs, err := org.getUserTeamIDs(e, userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &accessibleReposEnv{ return &accessibleReposEnv{
org: org, org: org,
userID: userID, user: user,
teamIDs: teamIDs, teamIDs: teamIDs,
e: e, e: e,
orderBy: SearchOrderByRecentUpdated, orderBy: SearchOrderByRecentUpdated,
@ -763,9 +773,12 @@ func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvi
} }
func (env *accessibleReposEnv) cond() builder.Cond { func (env *accessibleReposEnv) cond() builder.Cond {
var cond builder.Cond = builder.Eq{ var cond = builder.NewCond()
"`repository`.owner_id": env.org.ID, if env.user != nil && !env.user.IsRestricted {
"`repository`.is_private": false, cond = cond.Or(builder.Eq{
"`repository`.owner_id": env.org.ID,
"`repository`.is_private": false,
})
} }
if len(env.teamIDs) > 0 { if len(env.teamIDs) > 0 {
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))

View file

@ -111,8 +111,7 @@ func (repos MirrorRepositoryList) LoadAttributes() error {
// SearchRepoOptions holds the search options // SearchRepoOptions holds the search options
type SearchRepoOptions struct { type SearchRepoOptions struct {
UserID int64 Actor *User
UserIsAdmin bool
Keyword string Keyword string
OwnerID int64 OwnerID int64
PriorityOwnerID int64 PriorityOwnerID int64
@ -180,9 +179,9 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
var cond = builder.NewCond() var cond = builder.NewCond()
if opts.Private { 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 // OK we're in the context of a User
cond = cond.And(accessibleRepositoryCondition(opts.UserID)) cond = cond.And(accessibleRepositoryCondition(opts.Actor))
} }
} else { } else {
// Not looking at private organisations // 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}) 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 { if len(opts.OrderBy) == 0 {
opts.OrderBy = SearchOrderByAlphabetically opts.OrderBy = SearchOrderByAlphabetically
} }
@ -314,32 +317,39 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) {
} }
// accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible // accessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
func accessibleRepositoryCondition(userID int64) builder.Cond { func accessibleRepositoryCondition(user *User) builder.Cond {
return builder.Or( var cond = builder.NewCond()
if user == nil || !user.IsRestricted {
// 1. Be able to see all non-private repositories that either: // 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.Eq{"`repository`.is_private": false},
builder.Or( builder.Or(
// A. Aren't in organisations __OR__ // A. Aren't in organisations __OR__
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"type": UserTypeOrganization})), 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) // 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}))), builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePrivate})))))
), }
if user != nil {
// 2. Be able to see all repositories that we have access to // 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"). builder.In("`repository`.id", builder.Select("repo_id").
From("`access`"). From("`access`").
Where(builder.And( Where(builder.And(
builder.Eq{"user_id": userID}, builder.Eq{"user_id": user.ID},
builder.Gt{"mode": int(AccessModeNone)}))), builder.Gt{"mode": int(AccessModeNone)}))),
builder.In("`repository`.id", builder.Select("id"). builder.In("`repository`.id", builder.Select("id").
From("`repository`"). 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 // 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"). 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"))) Join("INNER", "team_user", "`team_user`.team_id = `team_repo`.team_id")))
}
return cond
} }
// SearchRepositoryByName takes keyword and part of repository name to search, // SearchRepositoryByName takes keyword and part of repository name to search,
@ -349,25 +359,18 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err
return SearchRepository(opts) 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 // FindUserAccessibleRepoIDs find all accessible repositories' ID by user's id
func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) { func FindUserAccessibleRepoIDs(user *User) ([]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},
),
)
}
repoIDs := make([]int64, 0, 10) repoIDs := make([]int64, 0, 10)
if err := x. if err := x.
Table("repository"). Table("repository").
Cols("id"). Cols("id").
Where(accessCond). Where(accessibleRepositoryCondition(user)).
Find(&repoIDs); err != nil { Find(&repoIDs); err != nil {
return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err) return nil, fmt.Errorf("FindUserAccesibleRepoIDs: %v", err)
} }

View file

@ -202,7 +202,7 @@ func getUserRepoPermission(e Engine, repo *Repository, user *User) (perm Permiss
} }
// plain user // plain user
perm.AccessMode, err = accessLevel(e, user.ID, repo) perm.AccessMode, err = accessLevel(e, user, repo)
if err != nil { if err != nil {
return 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. // for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
if !found && !repo.IsPrivate { if !found && !repo.IsPrivate && !user.IsRestricted {
if _, ok := perm.UnitsMode[u.Type]; !ok { if _, ok := perm.UnitsMode[u.Type]; !ok {
perm.UnitsMode[u.Type] = AccessModeRead perm.UnitsMode[u.Type] = AccessModeRead
} }
@ -284,7 +284,7 @@ func isUserRepoAdmin(e Engine, repo *Repository, user *User) (bool, error) {
return true, nil return true, nil
} }
mode, err := accessLevel(e, user.ID, repo) mode, err := accessLevel(e, user, repo)
if err != nil { if err != nil {
return false, err return false, err
} }

View file

@ -132,6 +132,7 @@ type User struct {
// Permissions // Permissions
IsActive bool `xorm:"INDEX"` // Activate primary email IsActive bool `xorm:"INDEX"` // Activate primary email
IsAdmin bool IsAdmin bool
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool // Allow migrate repository by local path AllowImportLocal bool // Allow migrate repository by local path
AllowCreateOrganization bool `xorm:"DEFAULT true"` AllowCreateOrganization bool `xorm:"DEFAULT true"`
@ -641,7 +642,7 @@ func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) {
if err := x.Table("repository"). if err := x.Table("repository").
Cols("repository.id"). Cols("repository.id").
Join("INNER", "team_user", "repository.owner_id = team_user.org_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). Where("team_user.uid = ?", u.ID).
GroupBy("repository.id").Find(&ids); err != nil { GroupBy("repository.id").Find(&ids); err != nil {
return nil, err return nil, err
@ -1470,7 +1471,7 @@ type SearchUserOptions struct {
OrderBy SearchOrderBy OrderBy SearchOrderBy
Page int Page int
Private bool // Include private orgs in search Private bool // Include private orgs in search
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 PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
IsActive util.OptionalBool IsActive util.OptionalBool
SearchByEmail bool // Search by email as well as username/full name SearchByEmail bool // Search by email as well as username/full name
@ -1497,7 +1498,7 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
cond = cond.And(builder.In("visibility", structs.VisibleTypePublic)) cond = cond.And(builder.In("visibility", structs.VisibleTypePublic))
} }
if opts.OwnerID > 0 { if opts.Actor != nil && opts.Type == UserTypeOrganization {
var exprCond builder.Cond var exprCond builder.Cond
if setting.Database.UseMySQL { if setting.Database.UseMySQL {
exprCond = builder.Expr("org_user.org_id = user.id") exprCond = builder.Expr("org_user.org_id = user.id")
@ -1506,9 +1507,15 @@ func (opts *SearchUserOptions) toConds() builder.Cond {
} else { } else {
exprCond = builder.Expr("org_user.org_id = \"user\".id") exprCond = builder.Expr("org_user.org_id = \"user\".id")
} }
accessCond := builder.Or( var accessCond = builder.NewCond()
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}))), if !opts.Actor.IsRestricted {
builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) 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) cond = cond.And(accessCond)
} }

View file

@ -30,11 +30,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
// get the action for comparison // get the action for comparison
actions, err := GetFeeds(GetFeedsOptions{ actions, err := GetFeeds(GetFeedsOptions{
RequestedUser: user, RequestedUser: user,
RequestingUserID: user.ID, Actor: user,
IncludePrivate: true, IncludePrivate: true,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: true, IncludeDeleted: true,
}) })
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -153,13 +153,13 @@ func TestSearchUsers(t *testing.T) {
} }
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, 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}, testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse},
[]int64{9}) []int64{9})
testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue}, 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}, testUserSuccess(&SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", Page: 1, IsActive: util.OptionalBoolTrue},
[]int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) []int64{1, 10, 11, 12, 13, 14, 15, 16, 18})

View file

@ -37,6 +37,7 @@ type AdminEditUserForm struct {
MaxRepoCreation int MaxRepoCreation int
Active bool Active bool
Admin bool Admin bool
Restricted bool
AllowGitHook bool AllowGitHook bool
AllowImportLocal bool AllowImportLocal bool
AllowCreateOrganization bool AllowCreateOrganization bool

View file

@ -1748,6 +1748,7 @@ users.new_account = Create User Account
users.name = Username users.name = Username
users.activated = Activated users.activated = Activated
users.admin = Admin users.admin = Admin
users.restricted = Restricted
users.repos = Repos users.repos = Repos
users.created = Created users.created = Created
users.last_login = Last Sign-In users.last_login = Last Sign-In
@ -1766,6 +1767,7 @@ users.max_repo_creation_desc = (Enter -1 to use the global default limit.)
users.is_activated = User Account Is Activated users.is_activated = User Account Is Activated
users.prohibit_login = Disable Sign-In users.prohibit_login = Disable Sign-In
users.is_admin = Is Administrator users.is_admin = Is Administrator
users.is_restricted = Is Restricted
users.allow_git_hook = May Create Git Hooks users.allow_git_hook = May Create Git Hooks
users.allow_import_local = May Import Local Repositories users.allow_import_local = May Import Local Repositories
users.allow_create_organization = May Create Organizations users.allow_create_organization = May Create Organizations

View file

@ -233,6 +233,7 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) {
u.MaxRepoCreation = form.MaxRepoCreation u.MaxRepoCreation = form.MaxRepoCreation
u.IsActive = form.Active u.IsActive = form.Active
u.IsAdmin = form.Admin u.IsAdmin = form.Admin
u.IsRestricted = form.Restricted
u.AllowGitHook = form.AllowGitHook u.AllowGitHook = form.AllowGitHook
u.AllowImportLocal = form.AllowImportLocal u.AllowImportLocal = form.AllowImportLocal
u.AllowCreateOrganization = form.AllowCreateOrganization u.AllowCreateOrganization = form.AllowCreateOrganization

View file

@ -77,8 +77,7 @@ func SearchIssues(ctx *context.APIContext) {
OwnerID: ctx.User.ID, OwnerID: ctx.User.ID,
TopicOnly: false, TopicOnly: false,
Collaborate: util.OptionalBoolNone, Collaborate: util.OptionalBoolNone,
UserIsAdmin: ctx.IsUserSiteAdmin(), Actor: ctx.User,
UserID: ctx.User.ID,
OrderBy: models.SearchOrderByRecentUpdated, OrderBy: models.SearchOrderByRecentUpdated,
}) })
if err != nil { if err != nil {

View file

@ -126,6 +126,7 @@ func Search(ctx *context.APIContext) {
// "$ref": "#/responses/validationError" // "$ref": "#/responses/validationError"
opts := &models.SearchRepoOptions{ opts := &models.SearchRepoOptions{
Actor: ctx.User,
Keyword: strings.Trim(ctx.Query("q"), " "), Keyword: strings.Trim(ctx.Query("q"), " "),
OwnerID: ctx.QueryInt64("uid"), OwnerID: ctx.QueryInt64("uid"),
PriorityOwnerID: ctx.QueryInt64("priority_owner_id"), PriorityOwnerID: ctx.QueryInt64("priority_owner_id"),
@ -135,8 +136,6 @@ func Search(ctx *context.APIContext) {
Collaborate: util.OptionalBoolNone, Collaborate: util.OptionalBoolNone,
Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")), Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),
Template: util.OptionalBoolNone, Template: util.OptionalBoolNone,
UserIsAdmin: ctx.IsUserSiteAdmin(),
UserID: ctx.Data["SignedUserID"].(int64),
StarredByID: ctx.QueryInt64("starredBy"), StarredByID: ctx.QueryInt64("starredBy"),
IncludeDescription: ctx.QueryBool("includeDesc"), IncludeDescription: ctx.QueryBool("includeDesc"),
} }

View file

@ -71,10 +71,11 @@ func Home(ctx *context.Context) {
// RepoSearchOptions when calling search repositories // RepoSearchOptions when calling search repositories
type RepoSearchOptions struct { type RepoSearchOptions struct {
OwnerID int64 OwnerID int64
Private bool Private bool
PageSize int Restricted bool
TplName base.TplName PageSize int
TplName base.TplName
} }
var ( var (
@ -135,6 +136,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
ctx.Data["TopicOnly"] = topicOnly ctx.Data["TopicOnly"] = topicOnly
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
Actor: ctx.User,
Page: page, Page: page,
PageSize: opts.PageSize, PageSize: opts.PageSize,
OrderBy: orderBy, OrderBy: orderBy,
@ -189,6 +191,7 @@ func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplN
if opts.Page <= 1 { if opts.Page <= 1 {
opts.Page = 1 opts.Page = 1
} }
opts.Actor = ctx.User
var ( var (
users []*models.User users []*models.User
@ -260,16 +263,10 @@ func ExploreOrganizations(ctx *context.Context) {
ctx.Data["PageIsExploreOrganizations"] = true ctx.Data["PageIsExploreOrganizations"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
var ownerID int64
if ctx.User != nil && !ctx.User.IsAdmin {
ownerID = ctx.User.ID
}
RenderUserSearch(ctx, &models.SearchUserOptions{ RenderUserSearch(ctx, &models.SearchUserOptions{
Type: models.UserTypeOrganization, Type: models.UserTypeOrganization,
PageSize: setting.UI.ExplorePagingNum, PageSize: setting.UI.ExplorePagingNum,
Private: ctx.User != nil, Private: ctx.User != nil,
OwnerID: ownerID,
}, tplExploreOrganizations) }, tplExploreOrganizations)
} }
@ -304,7 +301,7 @@ func ExploreCode(ctx *context.Context) {
// guest user or non-admin user // guest user or non-admin user
if ctx.User == nil || !isAdmin { if ctx.User == nil || !isAdmin {
repoIDs, err = models.FindUserAccessibleRepoIDs(userID) repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
if err != nil { if err != nil {
ctx.ServerError("SearchResults", err) ctx.ServerError("SearchResults", err)
return return

View file

@ -80,8 +80,7 @@ func Home(ctx *context.Context) {
OwnerID: org.ID, OwnerID: org.ID,
OrderBy: orderBy, OrderBy: orderBy,
Private: ctx.IsSigned, Private: ctx.IsSigned,
UserIsAdmin: ctx.IsUserSiteAdmin(), Actor: ctx.User,
UserID: ctx.Data["SignedUserID"].(int64),
Page: page, Page: page,
IsProfile: true, IsProfile: true,
PageSize: setting.UI.User.RepoPagingNum, PageSize: setting.UI.User.RepoPagingNum,

View file

@ -144,6 +144,7 @@ func Dashboard(ctx *context.Context) {
retrieveFeeds(ctx, models.GetFeedsOptions{ retrieveFeeds(ctx, models.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
Actor: ctx.User,
IncludePrivate: true, IncludePrivate: true,
OnlyPerformedBy: false, OnlyPerformedBy: false,
IncludeDeleted: false, IncludeDeleted: false,

View file

@ -161,6 +161,7 @@ func Profile(ctx *context.Context) {
switch tab { switch tab {
case "activity": case "activity":
retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser, retrieveFeeds(ctx, models.GetFeedsOptions{RequestedUser: ctxUser,
Actor: ctx.User,
IncludePrivate: showPrivate, IncludePrivate: showPrivate,
OnlyPerformedBy: true, OnlyPerformedBy: true,
IncludeDeleted: false, IncludeDeleted: false,
@ -171,11 +172,10 @@ func Profile(ctx *context.Context) {
case "stars": case "stars":
ctx.Data["PageIsProfileStarList"] = true ctx.Data["PageIsProfileStarList"] = true
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
Actor: ctx.User,
Keyword: keyword, Keyword: keyword,
OrderBy: orderBy, OrderBy: orderBy,
Private: ctx.IsSigned, Private: ctx.IsSigned,
UserIsAdmin: ctx.IsUserSiteAdmin(),
UserID: ctx.Data["SignedUserID"].(int64),
Page: page, Page: page,
PageSize: setting.UI.User.RepoPagingNum, PageSize: setting.UI.User.RepoPagingNum,
StarredByID: ctxUser.ID, StarredByID: ctxUser.ID,
@ -191,12 +191,11 @@ func Profile(ctx *context.Context) {
total = int(count) total = int(count)
default: default:
repos, count, err = models.SearchRepository(&models.SearchRepoOptions{ repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
Actor: ctx.User,
Keyword: keyword, Keyword: keyword,
OwnerID: ctxUser.ID, OwnerID: ctxUser.ID,
OrderBy: orderBy, OrderBy: orderBy,
Private: ctx.IsSigned, Private: ctx.IsSigned,
UserIsAdmin: ctx.IsUserSiteAdmin(),
UserID: ctx.Data["SignedUserID"].(int64),
Page: page, Page: page,
IsProfile: true, IsProfile: true,
PageSize: setting.UI.User.RepoPagingNum, PageSize: setting.UI.User.RepoPagingNum,

View file

@ -83,6 +83,12 @@
<input name="admin" type="checkbox" {{if .User.IsAdmin}}checked{{end}}> <input name="admin" type="checkbox" {{if .User.IsAdmin}}checked{{end}}>
</div> </div>
</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="inline field">
<div class="ui checkbox"> <div class="ui checkbox">
<label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label> <label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label>

View file

@ -21,6 +21,7 @@
<th>{{.i18n.Tr "email"}}</th> <th>{{.i18n.Tr "email"}}</th>
<th>{{.i18n.Tr "admin.users.activated"}}</th> <th>{{.i18n.Tr "admin.users.activated"}}</th>
<th>{{.i18n.Tr "admin.users.admin"}}</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.repos"}}</th>
<th>{{.i18n.Tr "admin.users.created"}}</th> <th>{{.i18n.Tr "admin.users.created"}}</th>
<th>{{.i18n.Tr "admin.users.last_login"}}</th> <th>{{.i18n.Tr "admin.users.last_login"}}</th>
@ -35,6 +36,7 @@
<td><span class="text truncate email">{{.Email}}</span></td> <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 .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 .IsAdmin}}-check{{end}}-square-o"></i></td>
<td><i class="fa fa{{if .IsRestricted}}-check{{end}}-square-o"></i></td>
<td>{{.NumRepos}}</td> <td>{{.NumRepos}}</td>
<td><span title="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</span></td> <td><span title="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</span></td>
{{if .LastLoginUnix}} {{if .LastLoginUnix}}