1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2019 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package models
7
8import (
9	"context"
10	"fmt"
11
12	"code.gitea.io/gitea/models/db"
13	"code.gitea.io/gitea/models/perm"
14	repo_model "code.gitea.io/gitea/models/repo"
15	user_model "code.gitea.io/gitea/models/user"
16)
17
18// Access represents the highest access level of a user to the repository. The only access type
19// that is not in this table is the real owner of a repository. In case of an organization
20// repository, the members of the owners team are in this table.
21type Access struct {
22	ID     int64 `xorm:"pk autoincr"`
23	UserID int64 `xorm:"UNIQUE(s)"`
24	RepoID int64 `xorm:"UNIQUE(s)"`
25	Mode   perm.AccessMode
26}
27
28func init() {
29	db.RegisterModel(new(Access))
30}
31
32func accessLevel(e db.Engine, user *user_model.User, repo *repo_model.Repository) (perm.AccessMode, error) {
33	mode := perm.AccessModeNone
34	var userID int64
35	restricted := false
36
37	if user != nil {
38		userID = user.ID
39		restricted = user.IsRestricted
40	}
41
42	if !restricted && !repo.IsPrivate {
43		mode = perm.AccessModeRead
44	}
45
46	if userID == 0 {
47		return mode, nil
48	}
49
50	if userID == repo.OwnerID {
51		return perm.AccessModeOwner, nil
52	}
53
54	a := &Access{UserID: userID, RepoID: repo.ID}
55	if has, err := e.Get(a); !has || err != nil {
56		return mode, err
57	}
58	return a.Mode, nil
59}
60
61func maxAccessMode(modes ...perm.AccessMode) perm.AccessMode {
62	max := perm.AccessModeNone
63	for _, mode := range modes {
64		if mode > max {
65			max = mode
66		}
67	}
68	return max
69}
70
71type userAccess struct {
72	User *user_model.User
73	Mode perm.AccessMode
74}
75
76// updateUserAccess updates an access map so that user has at least mode
77func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mode perm.AccessMode) {
78	if ua, ok := accessMap[user.ID]; ok {
79		ua.Mode = maxAccessMode(ua.Mode, mode)
80	} else {
81		accessMap[user.ID] = &userAccess{User: user, Mode: mode}
82	}
83}
84
85// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
86func refreshAccesses(e db.Engine, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
87	minMode := perm.AccessModeRead
88	if !repo.IsPrivate {
89		minMode = perm.AccessModeWrite
90	}
91
92	newAccesses := make([]Access, 0, len(accessMap))
93	for userID, ua := range accessMap {
94		if ua.Mode < minMode && !ua.User.IsRestricted {
95			continue
96		}
97
98		newAccesses = append(newAccesses, Access{
99			UserID: userID,
100			RepoID: repo.ID,
101			Mode:   ua.Mode,
102		})
103	}
104
105	// Delete old accesses and insert new ones for repository.
106	if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil {
107		return fmt.Errorf("delete old accesses: %v", err)
108	}
109	if len(newAccesses) == 0 {
110		return nil
111	}
112
113	if _, err = e.Insert(newAccesses); err != nil {
114		return fmt.Errorf("insert new accesses: %v", err)
115	}
116	return nil
117}
118
119// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
120func refreshCollaboratorAccesses(e db.Engine, repoID int64, accessMap map[int64]*userAccess) error {
121	collaborators, err := getCollaborators(e, repoID, db.ListOptions{})
122	if err != nil {
123		return fmt.Errorf("getCollaborations: %v", err)
124	}
125	for _, c := range collaborators {
126		if c.User.IsGhost() {
127			continue
128		}
129		updateUserAccess(accessMap, c.User, c.Collaboration.Mode)
130	}
131	return nil
132}
133
134// recalculateTeamAccesses recalculates new accesses for teams of an organization
135// except the team whose ID is given. It is used to assign a team ID when
136// remove repository from that team.
137func recalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, ignTeamID int64) (err error) {
138	accessMap := make(map[int64]*userAccess, 20)
139
140	if err = repo.GetOwner(ctx); err != nil {
141		return err
142	} else if !repo.Owner.IsOrganization() {
143		return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
144	}
145
146	e := db.GetEngine(ctx)
147
148	if err = refreshCollaboratorAccesses(e, repo.ID, accessMap); err != nil {
149		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
150	}
151
152	teams, err := OrgFromUser(repo.Owner).loadTeams(e)
153	if err != nil {
154		return err
155	}
156
157	for _, t := range teams {
158		if t.ID == ignTeamID {
159			continue
160		}
161
162		// Owner team gets owner access, and skip for teams that do not
163		// have relations with repository.
164		if t.IsOwnerTeam() {
165			t.AccessMode = perm.AccessModeOwner
166		} else if !t.hasRepository(e, repo.ID) {
167			continue
168		}
169
170		if err = t.getMembers(e); err != nil {
171			return fmt.Errorf("getMembers '%d': %v", t.ID, err)
172		}
173		for _, m := range t.Members {
174			updateUserAccess(accessMap, m, t.AccessMode)
175		}
176	}
177
178	return refreshAccesses(e, repo, accessMap)
179}
180
181// recalculateUserAccess recalculates new access for a single user
182// Usable if we know access only affected one user
183func recalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) {
184	minMode := perm.AccessModeRead
185	if !repo.IsPrivate {
186		minMode = perm.AccessModeWrite
187	}
188
189	accessMode := perm.AccessModeNone
190	e := db.GetEngine(ctx)
191	collaborator, err := getCollaboration(e, repo.ID, uid)
192	if err != nil {
193		return err
194	} else if collaborator != nil {
195		accessMode = collaborator.Mode
196	}
197
198	if err = repo.GetOwner(ctx); err != nil {
199		return err
200	} else if repo.Owner.IsOrganization() {
201		var teams []Team
202		if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id").
203			Join("INNER", "team_user", "team_user.team_id = team.id").
204			Where("team.org_id = ?", repo.OwnerID).
205			And("team_repo.repo_id=?", repo.ID).
206			And("team_user.uid=?", uid).
207			Find(&teams); err != nil {
208			return err
209		}
210
211		for _, t := range teams {
212			if t.IsOwnerTeam() {
213				t.AccessMode = perm.AccessModeOwner
214			}
215
216			accessMode = maxAccessMode(accessMode, t.AccessMode)
217		}
218	}
219
220	// Delete old user accesses and insert new one for repository.
221	if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil {
222		return fmt.Errorf("delete old user accesses: %v", err)
223	} else if accessMode >= minMode {
224		if _, err = e.Insert(&Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil {
225			return fmt.Errorf("insert new user accesses: %v", err)
226		}
227	}
228	return nil
229}
230
231func recalculateAccesses(ctx context.Context, repo *repo_model.Repository) error {
232	if repo.Owner.IsOrganization() {
233		return recalculateTeamAccesses(ctx, repo, 0)
234	}
235
236	e := db.GetEngine(ctx)
237	accessMap := make(map[int64]*userAccess, 20)
238	if err := refreshCollaboratorAccesses(e, repo.ID, accessMap); err != nil {
239		return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
240	}
241	return refreshAccesses(e, repo, accessMap)
242}
243
244// RecalculateAccesses recalculates all accesses for repository.
245func RecalculateAccesses(repo *repo_model.Repository) error {
246	return recalculateAccesses(db.DefaultContext, repo)
247}
248