* 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>
master
@@ -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 certain email domains](#only-allow-certain-email-domains) | ||||
* [Only allow/block certain OpenID providers](#only-allow-block-certain-openid-providers) | * [Only allow/block certain OpenID providers](#only-allow-block-certain-openid-providers) | ||||
* [Issue only users](#issue-only-users) | * [Issue only users](#issue-only-users) | ||||
* [Restricted users](#restricted-users) | |||||
* [Enable Fail2ban](#enable-fail2ban) | * [Enable Fail2ban](#enable-fail2ban) | ||||
* [Adding custom themes](#how-to-add-use-custom-themes) | * [Adding custom themes](#how-to-add-use-custom-themes) | ||||
* [SSHD vs built-in SSH](#sshd-vs-built-in-ssh) | * [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 | ### Issue only users | ||||
The current way to achieve this is to create/modify a user with a max repo creation limit of 0. | 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 | ### 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 | 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 | 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 | ||||
} | } | ||||
@@ -162,22 +170,37 @@ func maxAccessMode(modes ...AccessMode) AccessMode { | |||||
return max | 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? | // 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 | minMode := AccessModeRead | ||||
if !repo.IsPrivate { | if !repo.IsPrivate { | ||||
minMode = AccessModeWrite | minMode = AccessModeWrite | ||||
} | } | ||||
newAccesses := make([]Access, 0, len(accessMap)) | 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 | continue | ||||
} | } | ||||
newAccesses = append(newAccesses, Access{ | newAccesses = append(newAccesses, Access{ | ||||
UserID: userID, | UserID: userID, | ||||
RepoID: repo.ID, | 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. | // 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 { | if err != nil { | ||||
return fmt.Errorf("getCollaborations: %v", err) | 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 | 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 | // 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[int64]*userAccess, 20) | |||||
if err = repo.getOwner(e); err != nil { | if err = repo.getOwner(e); err != nil { | ||||
return err | return err | ||||
@@ -239,7 +262,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) | |||||
updateUserAccess(accessMap, m, t.Authorize) | |||||
} | } | ||||
} | } | ||||
@@ -300,7 +323,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[int64]*userAccess, 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) | ||||
} | } | ||||
@@ -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) | |||||
} |
@@ -284,11 +284,11 @@ func (a *Action) GetIssueContent() string { | |||||
// GetFeedsOptions options for retrieving feeds | // GetFeedsOptions options for retrieving feeds | ||||
type GetFeedsOptions struct { | 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 | // GetFeeds returns actions according to the provided options | ||||
@@ -296,8 +296,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) | ||||
} | } | ||||
@@ -306,6 +312,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}) | ||||
@@ -33,11 +33,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, | |||||
RequestingUserID: user.ID, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
RequestedUser: user, | |||||
Actor: user, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
}) | }) | ||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
if assert.Len(t, actions, 1) { | if assert.Len(t, actions, 1) { | ||||
@@ -46,10 +46,10 @@ func TestGetFeeds(t *testing.T) { | |||||
} | } | ||||
actions, err = GetFeeds(GetFeedsOptions{ | 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.NoError(t, err) | ||||
assert.Len(t, actions, 0) | assert.Len(t, actions, 0) | ||||
@@ -59,14 +59,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, | |||||
RequestingUserID: userID, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
RequestedUser: org, | |||||
Actor: user, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
}) | }) | ||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
assert.Len(t, actions, 1) | assert.Len(t, actions, 1) | ||||
@@ -76,11 +76,11 @@ func TestGetFeeds2(t *testing.T) { | |||||
} | } | ||||
actions, err = GetFeeds(GetFeedsOptions{ | 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.NoError(t, err) | ||||
assert.Len(t, actions, 0) | assert.Len(t, actions, 0) | ||||
@@ -74,4 +74,16 @@ | |||||
id: 13 | id: 13 | ||||
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 |
@@ -14,4 +14,10 @@ | |||||
id: 3 | id: 3 | ||||
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 |
@@ -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 |
@@ -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 | ||||
@@ -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 |
@@ -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 |
@@ -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 { | ||||
@@ -296,6 +296,8 @@ var migrations = []Migration{ | |||||
NewMigration("Fix migrated repositories' git service type", fixMigratedRepositoryServiceType), | NewMigration("Fix migrated repositories' git service type", fixMigratedRepositoryServiceType), | ||||
// v120 -> v121 | // v120 -> v121 | ||||
NewMigration("Add owner_name on table repository", addOwnerNameOnRepository), | NewMigration("Add owner_name on table repository", addOwnerNameOnRepository), | ||||
// v121 -> v122 | |||||
NewMigration("add is_restricted column for users table", addIsRestricted), | |||||
} | } | ||||
// Migrate database to current version | // Migrate database to current version | ||||
@@ -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 | 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 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{ | |||||
"`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 { | 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)) | ||||
@@ -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 | |||||
UserIsAdmin bool | |||||
Actor *User | |||||
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,43 @@ 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 { | |||||
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: | // 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) | |||||
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 | // 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 +363,18 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, err | |||||
return SearchRepository(opts) | return SearchRepository(opts) | ||||
} | } | ||||
// 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}, | |||||
), | |||||
) | |||||
} | |||||
// 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(user *User) ([]int64, error) { | |||||
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) | ||||
} | } | ||||
@@ -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. | |||||
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 { | 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 | ||||
} | } | ||||
@@ -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 | ||||
Visible []structs.VisibleType | 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 | 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 | ||||
@@ -1498,7 +1499,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 { | |||||
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") | ||||
@@ -1507,9 +1508,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( | |||||
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) | cond = cond.And(accessCond) | ||||
} | } | ||||
@@ -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, | |||||
RequestingUserID: user.ID, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
RequestedUser: user, | |||||
Actor: user, | |||||
IncludePrivate: true, | |||||
OnlyPerformedBy: false, | |||||
IncludeDeleted: true, | |||||
}) | }) | ||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
@@ -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}) | ||||
@@ -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 | ||||
@@ -1751,6 +1751,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 | ||||
@@ -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.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 | ||||
@@ -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 | ||||
@@ -73,13 +73,12 @@ func SearchIssues(ctx *context.APIContext) { | |||||
AllPublic: true, | AllPublic: true, | ||||
TopicOnly: false, | TopicOnly: false, | ||||
Collaborate: util.OptionalBoolNone, | Collaborate: util.OptionalBoolNone, | ||||
UserIsAdmin: ctx.IsUserSiteAdmin(), | |||||
OrderBy: models.SearchOrderByRecentUpdated, | OrderBy: models.SearchOrderByRecentUpdated, | ||||
Actor: ctx.User, | |||||
} | } | ||||
if ctx.IsSigned { | if ctx.IsSigned { | ||||
opts.Private = true | opts.Private = true | ||||
opts.AllLimited = true | opts.AllLimited = true | ||||
opts.UserID = ctx.User.ID | |||||
} | } | ||||
issueCount := 0 | issueCount := 0 | ||||
for page := 1; ; page++ { | for page := 1; ; page++ { | ||||
@@ -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"), | ||||
} | } | ||||
@@ -72,10 +72,11 @@ func Home(ctx *context.Context) { | |||||
// RepoSearchOptions when calling search repositories | // RepoSearchOptions when calling search repositories | ||||
type RepoSearchOptions struct { | type RepoSearchOptions struct { | ||||
OwnerID int64 | |||||
Private bool | |||||
PageSize int | |||||
TplName base.TplName | |||||
OwnerID int64 | |||||
Private bool | |||||
Restricted bool | |||||
PageSize int | |||||
TplName base.TplName | |||||
} | } | ||||
var ( | var ( | ||||
@@ -136,6 +137,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, | ||||
@@ -190,6 +192,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 | ||||
@@ -261,22 +264,16 @@ 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 | |||||
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, | Type: models.UserTypeOrganization, | ||||
PageSize: setting.UI.ExplorePagingNum, | 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 | // ExploreCode render explore code page | ||||
@@ -310,7 +307,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 | ||||
@@ -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(), | |||||
UserID: ctx.Data["SignedUserID"].(int64), | |||||
Actor: ctx.User, | |||||
Page: page, | Page: page, | ||||
IsProfile: true, | IsProfile: true, | ||||
PageSize: setting.UI.User.RepoPagingNum, | PageSize: setting.UI.User.RepoPagingNum, | ||||
@@ -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, | ||||
@@ -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, | ||||
@@ -85,6 +85,12 @@ | |||||
</div> | </div> | ||||
<div class="inline field"> | <div class="inline field"> | ||||
<div class="ui checkbox"> | <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> | <label><strong>{{.i18n.Tr "admin.users.allow_git_hook"}}</strong></label> | ||||
<input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}> | <input name="allow_git_hook" type="checkbox" {{if .User.CanEditGitHook}}checked{{end}} {{if DisableGitHooks}}disabled{{end}}> | ||||
</div> | </div> | ||||
@@ -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}} | ||||