Restricted users (#6274)
* Restricted users (#4334): initial implementation * Add User.IsRestricted & UI to edit it * Pass user object instead of user id to places where IsRestricted flag matters * Restricted users: maintain access rows for all referenced repos (incl public) * Take logged in user & IsRestricted flag into account in org/repo listings, searches and accesses * Add basic repo access tests for restricted users Signed-off-by: Manush Dodunekov <manush@stendahls.se> * Mention restricted users in the faq Signed-off-by: Manush Dodunekov <manush@stendahls.se> * Revert unnecessary change `.isUserPartOfOrg` -> `.IsUserPartOfOrg` Signed-off-by: Manush Dodunekov <manush@stendahls.se> * Remove unnecessary `org.IsOrganization()` call Signed-off-by: Manush Dodunekov <manush@stendahls.se> * Revert to an `int64` keyed `accessMap` * Add type `userAccess` * Add convenience func updateUserAccess() * Turn accessMap into a `map[int64]userAccess` Signed-off-by: Manush Dodunekov <manush@stendahls.se> * or even better: `map[int64]*userAccess` * updateUserAccess(): use tighter syntax as suggested by lafriks * even tighter * Avoid extra loop * Don't disclose limited orgs to unauthenticated users * Don't assume block only applies to orgs * Use an array of `VisibleType` for filtering * fix yet another thinko * Ok - no need for u * Revert "Ok - no need for u" This reverts commit 5c3e886aabd5acd997a3b35687d322439732c200. Co-authored-by: Antoine GIRARD <sapk@users.noreply.github.com> Co-authored-by: Lauris BH <lauris@nix.lv>
This commit is contained in:
parent
0b3aaa6196
commit
1751d5fcf2
31 changed files with 310 additions and 124 deletions
|
@ -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 {
|
for userID, ua := range accessMap {
|
||||||
if mode < minMode {
|
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 {
|
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]*userAccess) 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
|
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,8 +284,8 @@ func (a *Action) GetIssueContent() 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
|
||||||
|
@ -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})
|
||||||
|
|
|
@ -34,7 +34,7 @@ func TestGetFeeds(t *testing.T) {
|
||||||
|
|
||||||
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,
|
||||||
|
@ -47,7 +47,7 @@ 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,
|
||||||
})
|
})
|
||||||
|
@ -59,11 +59,11 @@ 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,
|
||||||
|
@ -77,7 +77,7 @@ 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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
17
models/migrations/v121.go
Normal file
17
models/migrations/v121.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
func addIsRestricted(x *xorm.Engine) error {
|
||||||
|
// User see models/user.go
|
||||||
|
type User struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync2(new(User))
|
||||||
|
}
|
|
@ -432,7 +432,7 @@ func hasOrgVisible(e Engine, org *User, user *User) bool {
|
||||||
return true
|
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{
|
var cond = builder.NewCond()
|
||||||
|
if env.user == nil || !env.user.IsRestricted {
|
||||||
|
cond = cond.Or(builder.Eq{
|
||||||
"`repository`.owner_id": env.org.ID,
|
"`repository`.owner_id": env.org.ID,
|
||||||
"`repository`.is_private": false,
|
"`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
|
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,34 +317,45 @@ 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 {
|
||||||
|
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)
|
// 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.Eq{"visibility": structs.VisibleTypePrivate}))),
|
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,
|
||||||
// it returns results in given range and number of total results.
|
// it returns results in given range and number of total results.
|
||||||
func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
func SearchRepositoryByName(opts *SearchRepoOptions) (RepositoryList, int64, error) {
|
||||||
|
@ -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
|
// AccessibleRepoIDsQuery queries accessible repository ids. Usable as a subquery wherever repo ids need to be filtered.
|
||||||
func FindUserAccessibleRepoIDs(userID int64) ([]int64, error) {
|
func (user *User) AccessibleRepoIDsQuery() *builder.Builder {
|
||||||
var accessCond builder.Cond = builder.Eq{"is_private": false}
|
return builder.Select("id").From("repository").Where(accessibleRepositoryCondition(user))
|
||||||
|
|
||||||
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},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
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 {
|
||||||
|
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))
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ 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,
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,7 @@ func Home(ctx *context.Context) {
|
||||||
type RepoSearchOptions struct {
|
type RepoSearchOptions struct {
|
||||||
OwnerID int64
|
OwnerID int64
|
||||||
Private bool
|
Private bool
|
||||||
|
Restricted bool
|
||||||
PageSize int
|
PageSize int
|
||||||
TplName base.TplName
|
TplName base.TplName
|
||||||
}
|
}
|
||||||
|
@ -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
|
visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
|
||||||
if ctx.User != nil && !ctx.User.IsAdmin {
|
if ctx.User != nil {
|
||||||
ownerID = ctx.User.ID
|
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,
|
Visible: visibleTypes,
|
||||||
}
|
}, tplExploreOrganizations)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(),
|
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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
Loading…
Reference in a new issue