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