code.gitea.io/gitea@v1.22.3/models/git/protected_branch.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package git 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "slices" 11 "strings" 12 13 "code.gitea.io/gitea/models/db" 14 "code.gitea.io/gitea/models/organization" 15 "code.gitea.io/gitea/models/perm" 16 access_model "code.gitea.io/gitea/models/perm/access" 17 repo_model "code.gitea.io/gitea/models/repo" 18 "code.gitea.io/gitea/models/unit" 19 user_model "code.gitea.io/gitea/models/user" 20 "code.gitea.io/gitea/modules/log" 21 "code.gitea.io/gitea/modules/timeutil" 22 "code.gitea.io/gitea/modules/util" 23 24 "github.com/gobwas/glob" 25 "github.com/gobwas/glob/syntax" 26 "xorm.io/builder" 27 ) 28 29 var ErrBranchIsProtected = errors.New("branch is protected") 30 31 // ProtectedBranch struct 32 type ProtectedBranch struct { 33 ID int64 `xorm:"pk autoincr"` 34 RepoID int64 `xorm:"UNIQUE(s)"` 35 Repo *repo_model.Repository `xorm:"-"` 36 RuleName string `xorm:"'branch_name' UNIQUE(s)"` // a branch name or a glob match to branch name 37 globRule glob.Glob `xorm:"-"` 38 isPlainName bool `xorm:"-"` 39 CanPush bool `xorm:"NOT NULL DEFAULT false"` 40 EnableWhitelist bool 41 WhitelistUserIDs []int64 `xorm:"JSON TEXT"` 42 WhitelistTeamIDs []int64 `xorm:"JSON TEXT"` 43 EnableMergeWhitelist bool `xorm:"NOT NULL DEFAULT false"` 44 WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` 45 MergeWhitelistUserIDs []int64 `xorm:"JSON TEXT"` 46 MergeWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` 47 EnableStatusCheck bool `xorm:"NOT NULL DEFAULT false"` 48 StatusCheckContexts []string `xorm:"JSON TEXT"` 49 EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"` 50 ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"` 51 ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"` 52 RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"` 53 BlockOnRejectedReviews bool `xorm:"NOT NULL DEFAULT false"` 54 BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"` 55 BlockOnOutdatedBranch bool `xorm:"NOT NULL DEFAULT false"` 56 DismissStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` 57 IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"` 58 RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"` 59 ProtectedFilePatterns string `xorm:"TEXT"` 60 UnprotectedFilePatterns string `xorm:"TEXT"` 61 62 CreatedUnix timeutil.TimeStamp `xorm:"created"` 63 UpdatedUnix timeutil.TimeStamp `xorm:"updated"` 64 } 65 66 func init() { 67 db.RegisterModel(new(ProtectedBranch)) 68 } 69 70 // IsRuleNameSpecial return true if it contains special character 71 func IsRuleNameSpecial(ruleName string) bool { 72 for i := 0; i < len(ruleName); i++ { 73 if syntax.Special(ruleName[i]) { 74 return true 75 } 76 } 77 return false 78 } 79 80 func (protectBranch *ProtectedBranch) loadGlob() { 81 if protectBranch.globRule == nil { 82 var err error 83 protectBranch.globRule, err = glob.Compile(protectBranch.RuleName, '/') 84 if err != nil { 85 log.Warn("Invalid glob rule for ProtectedBranch[%d]: %s %v", protectBranch.ID, protectBranch.RuleName, err) 86 protectBranch.globRule = glob.MustCompile(glob.QuoteMeta(protectBranch.RuleName), '/') 87 } 88 protectBranch.isPlainName = !IsRuleNameSpecial(protectBranch.RuleName) 89 } 90 } 91 92 // Match tests if branchName matches the rule 93 func (protectBranch *ProtectedBranch) Match(branchName string) bool { 94 protectBranch.loadGlob() 95 if protectBranch.isPlainName { 96 return strings.EqualFold(protectBranch.RuleName, branchName) 97 } 98 99 return protectBranch.globRule.Match(branchName) 100 } 101 102 func (protectBranch *ProtectedBranch) LoadRepo(ctx context.Context) (err error) { 103 if protectBranch.Repo != nil { 104 return nil 105 } 106 protectBranch.Repo, err = repo_model.GetRepositoryByID(ctx, protectBranch.RepoID) 107 return err 108 } 109 110 // CanUserPush returns if some user could push to this protected branch 111 func (protectBranch *ProtectedBranch) CanUserPush(ctx context.Context, user *user_model.User) bool { 112 if !protectBranch.CanPush { 113 return false 114 } 115 116 if !protectBranch.EnableWhitelist { 117 if err := protectBranch.LoadRepo(ctx); err != nil { 118 log.Error("LoadRepo: %v", err) 119 return false 120 } 121 122 writeAccess, err := access_model.HasAccessUnit(ctx, user, protectBranch.Repo, unit.TypeCode, perm.AccessModeWrite) 123 if err != nil { 124 log.Error("HasAccessUnit: %v", err) 125 return false 126 } 127 return writeAccess 128 } 129 130 if slices.Contains(protectBranch.WhitelistUserIDs, user.ID) { 131 return true 132 } 133 134 if len(protectBranch.WhitelistTeamIDs) == 0 { 135 return false 136 } 137 138 in, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.WhitelistTeamIDs) 139 if err != nil { 140 log.Error("IsUserInTeams: %v", err) 141 return false 142 } 143 return in 144 } 145 146 // IsUserMergeWhitelisted checks if some user is whitelisted to merge to this branch 147 func IsUserMergeWhitelisted(ctx context.Context, protectBranch *ProtectedBranch, userID int64, permissionInRepo access_model.Permission) bool { 148 if !protectBranch.EnableMergeWhitelist { 149 // Then we need to fall back on whether the user has write permission 150 return permissionInRepo.CanWrite(unit.TypeCode) 151 } 152 153 if slices.Contains(protectBranch.MergeWhitelistUserIDs, userID) { 154 return true 155 } 156 157 if len(protectBranch.MergeWhitelistTeamIDs) == 0 { 158 return false 159 } 160 161 in, err := organization.IsUserInTeams(ctx, userID, protectBranch.MergeWhitelistTeamIDs) 162 if err != nil { 163 log.Error("IsUserInTeams: %v", err) 164 return false 165 } 166 return in 167 } 168 169 // IsUserOfficialReviewer check if user is official reviewer for the branch (counts towards required approvals) 170 func IsUserOfficialReviewer(ctx context.Context, protectBranch *ProtectedBranch, user *user_model.User) (bool, error) { 171 repo, err := repo_model.GetRepositoryByID(ctx, protectBranch.RepoID) 172 if err != nil { 173 return false, err 174 } 175 176 if !protectBranch.EnableApprovalsWhitelist { 177 // Anyone with write access is considered official reviewer 178 writeAccess, err := access_model.HasAccessUnit(ctx, user, repo, unit.TypeCode, perm.AccessModeWrite) 179 if err != nil { 180 return false, err 181 } 182 return writeAccess, nil 183 } 184 185 if slices.Contains(protectBranch.ApprovalsWhitelistUserIDs, user.ID) { 186 return true, nil 187 } 188 189 inTeam, err := organization.IsUserInTeams(ctx, user.ID, protectBranch.ApprovalsWhitelistTeamIDs) 190 if err != nil { 191 return false, err 192 } 193 194 return inTeam, nil 195 } 196 197 // GetProtectedFilePatterns parses a semicolon separated list of protected file patterns and returns a glob.Glob slice 198 func (protectBranch *ProtectedBranch) GetProtectedFilePatterns() []glob.Glob { 199 return getFilePatterns(protectBranch.ProtectedFilePatterns) 200 } 201 202 // GetUnprotectedFilePatterns parses a semicolon separated list of unprotected file patterns and returns a glob.Glob slice 203 func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob { 204 return getFilePatterns(protectBranch.UnprotectedFilePatterns) 205 } 206 207 func getFilePatterns(filePatterns string) []glob.Glob { 208 extarr := make([]glob.Glob, 0, 10) 209 for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") { 210 expr = strings.TrimSpace(expr) 211 if expr != "" { 212 if g, err := glob.Compile(expr, '.', '/'); err != nil { 213 log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) 214 } else { 215 extarr = append(extarr, g) 216 } 217 } 218 } 219 return extarr 220 } 221 222 // MergeBlockedByProtectedFiles returns true if merge is blocked by protected files change 223 func (protectBranch *ProtectedBranch) MergeBlockedByProtectedFiles(changedProtectedFiles []string) bool { 224 glob := protectBranch.GetProtectedFilePatterns() 225 if len(glob) == 0 { 226 return false 227 } 228 229 return len(changedProtectedFiles) > 0 230 } 231 232 // IsProtectedFile return if path is protected 233 func (protectBranch *ProtectedBranch) IsProtectedFile(patterns []glob.Glob, path string) bool { 234 if len(patterns) == 0 { 235 patterns = protectBranch.GetProtectedFilePatterns() 236 if len(patterns) == 0 { 237 return false 238 } 239 } 240 241 lpath := strings.ToLower(strings.TrimSpace(path)) 242 243 r := false 244 for _, pat := range patterns { 245 if pat.Match(lpath) { 246 r = true 247 break 248 } 249 } 250 251 return r 252 } 253 254 // IsUnprotectedFile return if path is unprotected 255 func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, path string) bool { 256 if len(patterns) == 0 { 257 patterns = protectBranch.GetUnprotectedFilePatterns() 258 if len(patterns) == 0 { 259 return false 260 } 261 } 262 263 lpath := strings.ToLower(strings.TrimSpace(path)) 264 265 r := false 266 for _, pat := range patterns { 267 if pat.Match(lpath) { 268 r = true 269 break 270 } 271 } 272 273 return r 274 } 275 276 // GetProtectedBranchRuleByName getting protected branch rule by name 277 func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) { 278 // branch_name is legacy name, it actually is rule name 279 rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName}) 280 if err != nil { 281 return nil, err 282 } else if !exist { 283 return nil, nil 284 } 285 return rel, nil 286 } 287 288 // GetProtectedBranchRuleByID getting protected branch rule by rule ID 289 func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) { 290 rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID}) 291 if err != nil { 292 return nil, err 293 } else if !exist { 294 return nil, nil 295 } 296 return rel, nil 297 } 298 299 // WhitelistOptions represent all sorts of whitelists used for protected branches 300 type WhitelistOptions struct { 301 UserIDs []int64 302 TeamIDs []int64 303 304 MergeUserIDs []int64 305 MergeTeamIDs []int64 306 307 ApprovalsUserIDs []int64 308 ApprovalsTeamIDs []int64 309 } 310 311 // UpdateProtectBranch saves branch protection options of repository. 312 // If ID is 0, it creates a new record. Otherwise, updates existing record. 313 // This function also performs check if whitelist user and team's IDs have been changed 314 // to avoid unnecessary whitelist delete and regenerate. 315 func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) { 316 err = repo.MustNotBeArchived() 317 if err != nil { 318 return err 319 } 320 321 if err = repo.LoadOwner(ctx); err != nil { 322 return fmt.Errorf("LoadOwner: %v", err) 323 } 324 325 whitelist, err := updateUserWhitelist(ctx, repo, protectBranch.WhitelistUserIDs, opts.UserIDs) 326 if err != nil { 327 return err 328 } 329 protectBranch.WhitelistUserIDs = whitelist 330 331 whitelist, err = updateUserWhitelist(ctx, repo, protectBranch.MergeWhitelistUserIDs, opts.MergeUserIDs) 332 if err != nil { 333 return err 334 } 335 protectBranch.MergeWhitelistUserIDs = whitelist 336 337 whitelist, err = updateApprovalWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistUserIDs, opts.ApprovalsUserIDs) 338 if err != nil { 339 return err 340 } 341 protectBranch.ApprovalsWhitelistUserIDs = whitelist 342 343 // if the repo is in an organization 344 whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.WhitelistTeamIDs, opts.TeamIDs) 345 if err != nil { 346 return err 347 } 348 protectBranch.WhitelistTeamIDs = whitelist 349 350 whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.MergeWhitelistTeamIDs, opts.MergeTeamIDs) 351 if err != nil { 352 return err 353 } 354 protectBranch.MergeWhitelistTeamIDs = whitelist 355 356 whitelist, err = updateTeamWhitelist(ctx, repo, protectBranch.ApprovalsWhitelistTeamIDs, opts.ApprovalsTeamIDs) 357 if err != nil { 358 return err 359 } 360 protectBranch.ApprovalsWhitelistTeamIDs = whitelist 361 362 // Make sure protectBranch.ID is not 0 for whitelists 363 if protectBranch.ID == 0 { 364 if _, err = db.GetEngine(ctx).Insert(protectBranch); err != nil { 365 return fmt.Errorf("Insert: %v", err) 366 } 367 return nil 368 } 369 370 if _, err = db.GetEngine(ctx).ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil { 371 return fmt.Errorf("Update: %v", err) 372 } 373 374 return nil 375 } 376 377 // updateApprovalWhitelist checks whether the user whitelist changed and returns a whitelist with 378 // the users from newWhitelist which have explicit read or write access to the repo. 379 func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { 380 hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) 381 if !hasUsersChanged { 382 return currentWhitelist, nil 383 } 384 385 whitelist = make([]int64, 0, len(newWhitelist)) 386 for _, userID := range newWhitelist { 387 if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil { 388 return nil, err 389 } else if !reader { 390 continue 391 } 392 whitelist = append(whitelist, userID) 393 } 394 395 return whitelist, err 396 } 397 398 // updateUserWhitelist checks whether the user whitelist changed and returns a whitelist with 399 // the users from newWhitelist which have write access to the repo. 400 func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { 401 hasUsersChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) 402 if !hasUsersChanged { 403 return currentWhitelist, nil 404 } 405 406 whitelist = make([]int64, 0, len(newWhitelist)) 407 for _, userID := range newWhitelist { 408 user, err := user_model.GetUserByID(ctx, userID) 409 if err != nil { 410 return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) 411 } 412 perm, err := access_model.GetUserRepoPermission(ctx, repo, user) 413 if err != nil { 414 return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) 415 } 416 417 if !perm.CanWrite(unit.TypeCode) { 418 continue // Drop invalid user ID 419 } 420 421 whitelist = append(whitelist, userID) 422 } 423 424 return whitelist, err 425 } 426 427 // updateTeamWhitelist checks whether the team whitelist changed and returns a whitelist with 428 // the teams from newWhitelist which have write access to the repo. 429 func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, currentWhitelist, newWhitelist []int64) (whitelist []int64, err error) { 430 hasTeamsChanged := !util.SliceSortedEqual(currentWhitelist, newWhitelist) 431 if !hasTeamsChanged { 432 return currentWhitelist, nil 433 } 434 435 teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) 436 if err != nil { 437 return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) 438 } 439 440 whitelist = make([]int64, 0, len(teams)) 441 for i := range teams { 442 if slices.Contains(newWhitelist, teams[i].ID) { 443 whitelist = append(whitelist, teams[i].ID) 444 } 445 } 446 447 return whitelist, err 448 } 449 450 // DeleteProtectedBranch removes ProtectedBranch relation between the user and repository. 451 func DeleteProtectedBranch(ctx context.Context, repo *repo_model.Repository, id int64) (err error) { 452 err = repo.MustNotBeArchived() 453 if err != nil { 454 return err 455 } 456 457 protectedBranch := &ProtectedBranch{ 458 RepoID: repo.ID, 459 ID: id, 460 } 461 462 if affected, err := db.GetEngine(ctx).Delete(protectedBranch); err != nil { 463 return err 464 } else if affected != 1 { 465 return fmt.Errorf("delete protected branch ID(%v) failed", id) 466 } 467 468 return nil 469 } 470 471 // RemoveUserIDFromProtectedBranch remove all user ids from protected branch options 472 func RemoveUserIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, userID int64) error { 473 lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistUserIDs), len(p.ApprovalsWhitelistUserIDs), len(p.MergeWhitelistUserIDs) 474 p.WhitelistUserIDs = util.SliceRemoveAll(p.WhitelistUserIDs, userID) 475 p.ApprovalsWhitelistUserIDs = util.SliceRemoveAll(p.ApprovalsWhitelistUserIDs, userID) 476 p.MergeWhitelistUserIDs = util.SliceRemoveAll(p.MergeWhitelistUserIDs, userID) 477 478 if lenIDs != len(p.WhitelistUserIDs) || lenApprovalIDs != len(p.ApprovalsWhitelistUserIDs) || 479 lenMergeIDs != len(p.MergeWhitelistUserIDs) { 480 if _, err := db.GetEngine(ctx).ID(p.ID).Cols( 481 "whitelist_user_i_ds", 482 "merge_whitelist_user_i_ds", 483 "approvals_whitelist_user_i_ds", 484 ).Update(p); err != nil { 485 return fmt.Errorf("updateProtectedBranches: %v", err) 486 } 487 } 488 return nil 489 } 490 491 // RemoveTeamIDFromProtectedBranch remove all team ids from protected branch options 492 func RemoveTeamIDFromProtectedBranch(ctx context.Context, p *ProtectedBranch, teamID int64) error { 493 lenIDs, lenApprovalIDs, lenMergeIDs := len(p.WhitelistTeamIDs), len(p.ApprovalsWhitelistTeamIDs), len(p.MergeWhitelistTeamIDs) 494 p.WhitelistTeamIDs = util.SliceRemoveAll(p.WhitelistTeamIDs, teamID) 495 p.ApprovalsWhitelistTeamIDs = util.SliceRemoveAll(p.ApprovalsWhitelistTeamIDs, teamID) 496 p.MergeWhitelistTeamIDs = util.SliceRemoveAll(p.MergeWhitelistTeamIDs, teamID) 497 498 if lenIDs != len(p.WhitelistTeamIDs) || 499 lenApprovalIDs != len(p.ApprovalsWhitelistTeamIDs) || 500 lenMergeIDs != len(p.MergeWhitelistTeamIDs) { 501 if _, err := db.GetEngine(ctx).ID(p.ID).Cols( 502 "whitelist_team_i_ds", 503 "merge_whitelist_team_i_ds", 504 "approvals_whitelist_team_i_ds", 505 ).Update(p); err != nil { 506 return fmt.Errorf("updateProtectedBranches: %v", err) 507 } 508 } 509 return nil 510 }