code.gitea.io/gitea@v1.21.7/models/perm/access/access.go (about) 1 // Copyright 2014 The Gogs Authors. All rights reserved. 2 // Copyright 2019 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package access 6 7 import ( 8 "context" 9 "fmt" 10 11 "code.gitea.io/gitea/models/db" 12 "code.gitea.io/gitea/models/organization" 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. 21 type 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 28 func init() { 29 db.RegisterModel(new(Access)) 30 } 31 32 func accessLevel(ctx context.Context, 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 := db.GetByBean(ctx, a); !has || err != nil { 56 return mode, err 57 } 58 return a.Mode, nil 59 } 60 61 func 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 71 type 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 77 func 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? 86 func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) { 87 minMode := perm.AccessModeRead 88 if err := repo.LoadOwner(ctx); err != nil { 89 return fmt.Errorf("LoadOwner: %w", err) 90 } 91 92 // If the repo isn't private and isn't owned by a organization, 93 // increase the minMode to Write. 94 if !repo.IsPrivate && !repo.Owner.IsOrganization() { 95 minMode = perm.AccessModeWrite 96 } 97 98 newAccesses := make([]Access, 0, len(accessMap)) 99 for userID, ua := range accessMap { 100 if ua.Mode < minMode && !ua.User.IsRestricted { 101 continue 102 } 103 104 newAccesses = append(newAccesses, Access{ 105 UserID: userID, 106 RepoID: repo.ID, 107 Mode: ua.Mode, 108 }) 109 } 110 111 // Delete old accesses and insert new ones for repository. 112 if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil { 113 return fmt.Errorf("delete old accesses: %w", err) 114 } 115 if len(newAccesses) == 0 { 116 return nil 117 } 118 119 if err = db.Insert(ctx, newAccesses); err != nil { 120 return fmt.Errorf("insert new accesses: %w", err) 121 } 122 return nil 123 } 124 125 // refreshCollaboratorAccesses retrieves repository collaborations with their access modes. 126 func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap map[int64]*userAccess) error { 127 collaborators, err := repo_model.GetCollaborators(ctx, repoID, db.ListOptions{}) 128 if err != nil { 129 return fmt.Errorf("getCollaborations: %w", err) 130 } 131 for _, c := range collaborators { 132 if c.User.IsGhost() { 133 continue 134 } 135 updateUserAccess(accessMap, c.User, c.Collaboration.Mode) 136 } 137 return nil 138 } 139 140 // RecalculateTeamAccesses recalculates new accesses for teams of an organization 141 // except the team whose ID is given. It is used to assign a team ID when 142 // remove repository from that team. 143 func RecalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, ignTeamID int64) (err error) { 144 accessMap := make(map[int64]*userAccess, 20) 145 146 if err = repo.LoadOwner(ctx); err != nil { 147 return err 148 } else if !repo.Owner.IsOrganization() { 149 return fmt.Errorf("owner is not an organization: %d", repo.OwnerID) 150 } 151 152 if err = refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil { 153 return fmt.Errorf("refreshCollaboratorAccesses: %w", err) 154 } 155 156 teams, err := organization.FindOrgTeams(ctx, repo.Owner.ID) 157 if err != nil { 158 return err 159 } 160 161 for _, t := range teams { 162 if t.ID == ignTeamID { 163 continue 164 } 165 166 // Owner team gets owner access, and skip for teams that do not 167 // have relations with repository. 168 if t.IsOwnerTeam() { 169 t.AccessMode = perm.AccessModeOwner 170 } else if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) { 171 continue 172 } 173 174 if err = t.LoadMembers(ctx); err != nil { 175 return fmt.Errorf("getMembers '%d': %w", t.ID, err) 176 } 177 for _, m := range t.Members { 178 updateUserAccess(accessMap, m, t.AccessMode) 179 } 180 } 181 182 return refreshAccesses(ctx, repo, accessMap) 183 } 184 185 // RecalculateUserAccess recalculates new access for a single user 186 // Usable if we know access only affected one user 187 func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid int64) (err error) { 188 minMode := perm.AccessModeRead 189 if !repo.IsPrivate { 190 minMode = perm.AccessModeWrite 191 } 192 193 accessMode := perm.AccessModeNone 194 e := db.GetEngine(ctx) 195 collaborator, err := repo_model.GetCollaboration(ctx, repo.ID, uid) 196 if err != nil { 197 return err 198 } else if collaborator != nil { 199 accessMode = collaborator.Mode 200 } 201 202 if err = repo.LoadOwner(ctx); err != nil { 203 return err 204 } else if repo.Owner.IsOrganization() { 205 var teams []organization.Team 206 if err := e.Join("INNER", "team_repo", "team_repo.team_id = team.id"). 207 Join("INNER", "team_user", "team_user.team_id = team.id"). 208 Where("team.org_id = ?", repo.OwnerID). 209 And("team_repo.repo_id=?", repo.ID). 210 And("team_user.uid=?", uid). 211 Find(&teams); err != nil { 212 return err 213 } 214 215 for _, t := range teams { 216 if t.IsOwnerTeam() { 217 t.AccessMode = perm.AccessModeOwner 218 } 219 220 accessMode = maxAccessMode(accessMode, t.AccessMode) 221 } 222 } 223 224 // Delete old user accesses and insert new one for repository. 225 if _, err = e.Delete(&Access{RepoID: repo.ID, UserID: uid}); err != nil { 226 return fmt.Errorf("delete old user accesses: %w", err) 227 } else if accessMode >= minMode { 228 if err = db.Insert(ctx, &Access{RepoID: repo.ID, UserID: uid, Mode: accessMode}); err != nil { 229 return fmt.Errorf("insert new user accesses: %w", err) 230 } 231 } 232 return nil 233 } 234 235 // RecalculateAccesses recalculates all accesses for repository. 236 func RecalculateAccesses(ctx context.Context, repo *repo_model.Repository) error { 237 if repo.Owner.IsOrganization() { 238 return RecalculateTeamAccesses(ctx, repo, 0) 239 } 240 241 accessMap := make(map[int64]*userAccess, 20) 242 if err := refreshCollaboratorAccesses(ctx, repo.ID, accessMap); err != nil { 243 return fmt.Errorf("refreshCollaboratorAccesses: %w", err) 244 } 245 return refreshAccesses(ctx, repo, accessMap) 246 }