Skip to content

Commit

Permalink
Restricted users (#6274)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Mention restricted users in the faq

Signed-off-by: Manush Dodunekov <[email protected]>

* Revert unnecessary change `.isUserPartOfOrg` -> `.IsUserPartOfOrg`

Signed-off-by: Manush Dodunekov <[email protected]>

* Remove unnecessary `org.IsOrganization()` call

Signed-off-by: Manush Dodunekov <[email protected]>

* 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 <[email protected]>

* 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 5c3e886.

Co-authored-by: Antoine GIRARD <[email protected]>
Co-authored-by: Lauris BH <[email protected]>
  • Loading branch information
3 people committed Jan 13, 2020
1 parent 0b3aaa6 commit 1751d5f
Show file tree
Hide file tree
Showing 31 changed files with 310 additions and 124 deletions.
9 changes: 9 additions & 0 deletions docs/content/doc/help/faq.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Also see [Support Options]({{< relref "doc/help/seek-help.en-us.md" >}})
* [Only allow certain email domains](#only-allow-certain-email-domains)
* [Only allow/block certain OpenID providers](#only-allow-block-certain-openid-providers)
* [Issue only users](#issue-only-users)
* [Restricted users](#restricted-users)
* [Enable Fail2ban](#enable-fail2ban)
* [Adding custom themes](#how-to-add-use-custom-themes)
* [SSHD vs built-in SSH](#sshd-vs-built-in-ssh)
Expand Down Expand Up @@ -147,6 +148,14 @@ You can configure `WHITELISTED_URIS` or `BLACKLISTED_URIS` under `[openid]` in y
### Issue only users
The current way to achieve this is to create/modify a user with a max repo creation limit of 0.

### Restricted users
Restricted users are limited to a subset of the content based on their organization/team memberships and collaborations, ignoring the public flag on organizations/repos etc.__

Example use case: A company runs a Gitea instance that requires login. Most repos are public (accessible/browseable by all co-workers).

At some point, a customer or third party needs access to a specific repo and only that repo. Making such a customer account restricted and granting any needed access using team membership(s) and/or collaboration(s) is a simple way to achieve that without the need to make everything private.


### Enable Fail2ban

Use [Fail2Ban]({{ relref "doc/usage/fail2ban-setup.md" >}}) to monitor and stop automated login attempts or other malicious behavior based on log patterns
Expand Down
49 changes: 36 additions & 13 deletions models/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,17 @@ type Access struct {
Mode AccessMode
}

func accessLevel(e Engine, userID int64, repo *Repository) (AccessMode, error) {
func accessLevel(e Engine, user *User, repo *Repository) (AccessMode, error) {
mode := AccessModeNone
if !repo.IsPrivate {
var userID int64
restricted := false

if user != nil {
userID = user.ID
restricted = user.IsRestricted
}

if !restricted && !repo.IsPrivate {
mode = AccessModeRead
}

Expand Down Expand Up @@ -162,22 +170,37 @@ func maxAccessMode(modes ...AccessMode) AccessMode {
return max
}

type userAccess struct {
User *User
Mode AccessMode
}

// updateUserAccess updates an access map so that user has at least mode
func updateUserAccess(accessMap map[int64]*userAccess, user *User, mode AccessMode) {
if ua, ok := accessMap[user.ID]; ok {
ua.Mode = maxAccessMode(ua.Mode, mode)
} else {
accessMap[user.ID] = &userAccess{User: user, Mode: mode}
}
}

// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]*userAccess) (err error) {
minMode := AccessModeRead
if !repo.IsPrivate {
minMode = AccessModeWrite
}

newAccesses := make([]Access, 0, len(accessMap))
for userID, mode := range accessMap {
if mode < minMode {
for userID, ua := range accessMap {
if ua.Mode < minMode && !ua.User.IsRestricted {
continue
}

newAccesses = append(newAccesses, Access{
UserID: userID,
RepoID: repo.ID,
Mode: mode,
Mode: ua.Mode,
})
}

Expand All @@ -191,13 +214,13 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode
}

// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
collaborations, err := repo.getCollaborations(e)
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]*userAccess) error {
collaborators, err := repo.getCollaborators(e)
if err != nil {
return fmt.Errorf("getCollaborations: %v", err)
}
for _, c := range collaborations {
accessMap[c.UserID] = c.Mode
for _, c := range collaborators {
updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
}
return nil
}
Expand All @@ -206,7 +229,7 @@ func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int6
// except the team whose ID is given. It is used to assign a team ID when
// remove repository from that team.
func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
accessMap := make(map[int64]AccessMode, 20)
accessMap := make(map[int64]*userAccess, 20)

if err = repo.getOwner(e); err != nil {
return err
Expand Down Expand Up @@ -239,7 +262,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err
return fmt.Errorf("getMembers '%d': %v", t.ID, err)
}
for _, m := range t.Members {
accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
updateUserAccess(accessMap, m, t.Authorize)
}
}

Expand Down Expand Up @@ -300,7 +323,7 @@ func (repo *Repository) recalculateAccesses(e Engine) error {
return repo.recalculateTeamAccesses(e, 0)
}

accessMap := make(map[int64]AccessMode, 20)
accessMap := make(map[int64]*userAccess, 20)
if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
}
Expand Down
50 changes: 50 additions & 0 deletions models/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ func TestAccessLevel(t *testing.T) {

user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User)
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
// A public repository owned by User 2
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
assert.False(t, repo1.IsPrivate)
// A private repository owned by Org 3
repo3 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository)
assert.True(t, repo3.IsPrivate)

// Another public repository
repo4 := AssertExistsAndLoadBean(t, &Repository{ID: 4}).(*Repository)
assert.False(t, repo4.IsPrivate)
// org. owned private repo
repo24 := AssertExistsAndLoadBean(t, &Repository{ID: 24}).(*Repository)

level, err := AccessLevel(user2, repo1)
assert.NoError(t, err)
assert.Equal(t, AccessModeOwner, level)
Expand All @@ -37,6 +44,21 @@ func TestAccessLevel(t *testing.T) {
level, err = AccessLevel(user5, repo3)
assert.NoError(t, err)
assert.Equal(t, AccessModeNone, level)

// restricted user has no access to a public repo
level, err = AccessLevel(user29, repo1)
assert.NoError(t, err)
assert.Equal(t, AccessModeNone, level)

// ... unless he's a collaborator
level, err = AccessLevel(user29, repo4)
assert.NoError(t, err)
assert.Equal(t, AccessModeWrite, level)

// ... or a team member
level, err = AccessLevel(user29, repo24)
assert.NoError(t, err)
assert.Equal(t, AccessModeRead, level)
}

func TestHasAccess(t *testing.T) {
Expand Down Expand Up @@ -72,6 +94,11 @@ func TestUser_GetRepositoryAccesses(t *testing.T) {
accesses, err := user1.GetRepositoryAccesses()
assert.NoError(t, err)
assert.Len(t, accesses, 0)

user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
accesses, err = user29.GetRepositoryAccesses()
assert.NoError(t, err)
assert.Len(t, accesses, 2)
}

func TestUser_GetAccessibleRepositories(t *testing.T) {
Expand All @@ -86,6 +113,11 @@ func TestUser_GetAccessibleRepositories(t *testing.T) {
repos, err = user2.GetAccessibleRepositories(0)
assert.NoError(t, err)
assert.Len(t, repos, 1)

user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)
repos, err = user29.GetAccessibleRepositories(0)
assert.NoError(t, err)
assert.Len(t, repos, 2)
}

func TestRepository_RecalculateAccesses(t *testing.T) {
Expand Down Expand Up @@ -119,3 +151,21 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
assert.NoError(t, err)
assert.False(t, has)
}

func TestRepository_RecalculateAccesses3(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
team5 := AssertExistsAndLoadBean(t, &Team{ID: 5}).(*Team)
user29 := AssertExistsAndLoadBean(t, &User{ID: 29}).(*User)

has, err := x.Get(&Access{UserID: 29, RepoID: 23})
assert.NoError(t, err)
assert.False(t, has)

// adding user29 to team5 should add an explicit access row for repo 23
// even though repo 23 is public
assert.NoError(t, AddTeamMember(team5, user29.ID))

has, err = x.Get(&Access{UserID: 29, RepoID: 23})
assert.NoError(t, err)
assert.True(t, has)
}
20 changes: 14 additions & 6 deletions models/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,20 +284,26 @@ func (a *Action) GetIssueContent() string {

// GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct {
RequestedUser *User
RequestingUserID int64
IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions
RequestedUser *User // the user we want activity for
Actor *User // the user viewing the activity
IncludePrivate bool // include private actions
OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions
}

// GetFeeds returns actions according to the provided options
func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
cond := builder.NewCond()

var repoIDs []int64
var actorID int64

if opts.Actor != nil {
actorID = opts.Actor.ID
}

if opts.RequestedUser.IsOrganization() {
env, err := opts.RequestedUser.AccessibleReposEnv(opts.RequestingUserID)
env, err := opts.RequestedUser.AccessibleReposEnv(actorID)
if err != nil {
return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
}
Expand All @@ -306,6 +312,8 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
}

cond = cond.And(builder.In("repo_id", repoIDs))
} else if opts.Actor != nil {
cond = cond.And(builder.In("repo_id", opts.Actor.AccessibleRepoIDsQuery()))
}

cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
Expand Down
40 changes: 20 additions & 20 deletions models/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ func TestGetFeeds(t *testing.T) {
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

actions, err := GetFeeds(GetFeedsOptions{
RequestedUser: user,
RequestingUserID: user.ID,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
RequestedUser: user,
Actor: user,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
if assert.Len(t, actions, 1) {
Expand All @@ -46,10 +46,10 @@ func TestGetFeeds(t *testing.T) {
}

actions, err = GetFeeds(GetFeedsOptions{
RequestedUser: user,
RequestingUserID: user.ID,
IncludePrivate: false,
OnlyPerformedBy: false,
RequestedUser: user,
Actor: user,
IncludePrivate: false,
OnlyPerformedBy: false,
})
assert.NoError(t, err)
assert.Len(t, actions, 0)
Expand All @@ -59,14 +59,14 @@ func TestGetFeeds2(t *testing.T) {
// test with an organization user
assert.NoError(t, PrepareTestDatabase())
org := AssertExistsAndLoadBean(t, &User{ID: 3}).(*User)
const userID = 2 // user2 is an owner of the organization
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)

actions, err := GetFeeds(GetFeedsOptions{
RequestedUser: org,
RequestingUserID: userID,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
RequestedUser: org,
Actor: user,
IncludePrivate: true,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 1)
Expand All @@ -76,11 +76,11 @@ func TestGetFeeds2(t *testing.T) {
}

actions, err = GetFeeds(GetFeedsOptions{
RequestedUser: org,
RequestingUserID: userID,
IncludePrivate: false,
OnlyPerformedBy: false,
IncludeDeleted: true,
RequestedUser: org,
Actor: user,
IncludePrivate: false,
OnlyPerformedBy: false,
IncludeDeleted: true,
})
assert.NoError(t, err)
assert.Len(t, actions, 0)
Expand Down
14 changes: 13 additions & 1 deletion models/fixtures/access.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,16 @@
id: 13
user_id: 20
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
8 changes: 7 additions & 1 deletion models/fixtures/collaboration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@
id: 3
repo_id: 40
user_id: 4
mode: 2 # write
mode: 2 # write

-
id: 4
repo_id: 4
user_id: 29
mode: 2 # write
5 changes: 5 additions & 0 deletions models/fixtures/org_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,8 @@
org_id: 6
is_public: true

-
id: 11
uid: 29
org_id: 17
is_public: true
2 changes: 1 addition & 1 deletion models/fixtures/team.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
name: review_team
authorize: 1 # read
num_repos: 1
num_members: 1
num_members: 2

-
id: 10
Expand Down
6 changes: 6 additions & 0 deletions models/fixtures/team_user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,9 @@
org_id: 6
team_id: 13
uid: 28

-
id: 15
org_id: 17
team_id: 9
uid: 29
Loading

0 comments on commit 1751d5f

Please sign in to comment.