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