Skip to content

Restricted users #6274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jan 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
836f9d8
Restricted users (#4334): initial implementation
mnsh Apr 15, 2019
1f7b0fc
Merge branch 'master' into feature/restricted-users
sapk Jan 8, 2020
dc7e423
Mention restricted users in the faq
mnsh Jan 8, 2020
4501f22
Revert unnecessary change `.isUserPartOfOrg` -> `.IsUserPartOfOrg`
mnsh Jan 8, 2020
5b65242
Remove unnecessary `org.IsOrganization()` call
mnsh Jan 8, 2020
124f056
Revert to an `int64` keyed `accessMap`
mnsh Jan 8, 2020
06ad61a
or even better: `map[int64]*userAccess`
mnsh Jan 8, 2020
10de9b6
updateUserAccess(): use tighter syntax as suggested by lafriks
mnsh Jan 8, 2020
7d17aad
even tighter
mnsh Jan 8, 2020
7d74b85
Avoid extra loop
mnsh Jan 9, 2020
3459547
merge upstream, resolve migration conflicts
mnsh Jan 9, 2020
b788ef3
Don't disclose limited orgs to unauthenticated users
mnsh Jan 9, 2020
ee2f31f
Don't assume block only applies to orgs
mnsh Jan 9, 2020
eeecdab
Use an array of `VisibleType` for filtering
mnsh Jan 9, 2020
57744b5
merge upstream & fix conflicts
mnsh Jan 10, 2020
d775c3b
fix yet another thinko
mnsh Jan 11, 2020
5ae6016
Merge branch 'master' into feature/restricted-users
lafriks Jan 11, 2020
095bfa6
Merge master & resolve conflicts
mnsh Jan 12, 2020
5c3e886
Ok - no need for u
mnsh Jan 12, 2020
95bca8e
Revert "Ok - no need for u"
mnsh Jan 12, 2020
79c456e
Merge branch 'master' into feature/restricted-users
lafriks Jan 12, 2020
eaed6b3
Merge branch 'master' into feature/restricted-users
mnsh Jan 13, 2020
a54d9d6
Merge branch 'master' into feature/restricted-users
sapk Jan 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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