sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/cmd/branchprotector/protect.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "flag" 21 "fmt" 22 "net/url" 23 "os" 24 "regexp" 25 "sort" 26 "strings" 27 "sync" 28 29 "github.com/sirupsen/logrus" 30 31 utilerrors "k8s.io/apimachinery/pkg/util/errors" 32 "k8s.io/apimachinery/pkg/util/sets" 33 "sigs.k8s.io/prow/pkg/config" 34 "sigs.k8s.io/prow/pkg/flagutil" 35 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 36 "sigs.k8s.io/prow/pkg/github" 37 "sigs.k8s.io/prow/pkg/logrusutil" 38 ) 39 40 const ( 41 defaultTokens = 300 42 defaultBurst = 100 43 ) 44 45 type options struct { 46 config configflagutil.ConfigOptions 47 confirm bool 48 verifyRestrictions bool 49 enableAppsRestrictions bool 50 51 github flagutil.GitHubOptions 52 githubEnablement flagutil.GitHubEnablementOptions 53 } 54 55 func (o *options) Validate() error { 56 if err := o.github.Validate(!o.confirm); err != nil { 57 return err 58 } 59 60 if err := o.githubEnablement.Validate(!o.confirm); err != nil { 61 return err 62 } 63 64 if err := o.config.Validate(!o.confirm); err != nil { 65 return err 66 } 67 68 return nil 69 } 70 71 func gatherOptions() options { 72 o := options{} 73 fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 74 fs.BoolVar(&o.confirm, "confirm", false, "Mutate github if set") 75 fs.BoolVar(&o.verifyRestrictions, "verify-restrictions", false, "Verify the restrictions section of the request for authorized apps/collaborators/teams") 76 fs.BoolVar(&o.enableAppsRestrictions, "enable-apps-restrictions", false, "Enable feature to enforce apps restrictions in branch protection rules") 77 o.config.AddFlags(fs) 78 o.github.AddCustomizedFlags(fs, flagutil.ThrottlerDefaults(defaultTokens, defaultBurst)) 79 o.githubEnablement.AddFlags(fs) 80 fs.Parse(os.Args[1:]) 81 return o 82 } 83 84 type requirements struct { 85 Org string 86 Repo string 87 Branch string 88 Request *github.BranchProtectionRequest 89 } 90 91 // Errors holds a list of errors, including a method to concurrently append. 92 type Errors struct { 93 lock sync.Mutex 94 errs []error 95 } 96 97 func (e *Errors) add(err error) { 98 e.lock.Lock() 99 logrus.Info(err) 100 defer e.lock.Unlock() 101 e.errs = append(e.errs, err) 102 } 103 104 func main() { 105 logrusutil.ComponentInit() 106 107 o := gatherOptions() 108 if err := o.Validate(); err != nil { 109 logrus.Fatal(err) 110 } 111 112 ca, err := o.config.ConfigAgent() 113 if err != nil { 114 logrus.WithError(err).Fatalf("Failed to load --config-path=%s", o.config.ConfigPath) 115 } 116 cfg := ca.Config() 117 cfg.BranchProtectionWarnings(logrus.NewEntry(logrus.StandardLogger()), cfg.PresubmitsStatic) 118 119 githubClient, err := o.github.GitHubClient(!o.confirm) 120 if err != nil { 121 logrus.WithError(err).Fatal("Error getting GitHub client.") 122 } 123 124 p := protector{ 125 client: githubClient, 126 cfg: cfg, 127 updates: make(chan requirements), 128 errors: Errors{}, 129 completedRepos: make(map[string]bool), 130 done: make(chan []error), 131 verifyRestrictions: o.verifyRestrictions, 132 enableAppsRestrictions: o.enableAppsRestrictions, 133 enabled: o.githubEnablement.EnablementChecker(), 134 } 135 136 go p.configureBranches() 137 p.protect() 138 close(p.updates) 139 errors := <-p.done 140 if n := len(errors); n > 0 { 141 for i, err := range errors { 142 logrus.WithError(err).Error(i) 143 } 144 logrus.Fatalf("Encountered %d errors protecting branches", n) 145 } 146 } 147 148 type client interface { 149 GetBranchProtection(org, repo, branch string) (*github.BranchProtection, error) 150 RemoveBranchProtection(org, repo, branch string) error 151 UpdateBranchProtection(org, repo, branch string, config github.BranchProtectionRequest) error 152 GetBranches(org, repo string, onlyProtected bool) ([]github.Branch, error) 153 GetRepo(owner, name string) (github.FullRepo, error) 154 GetRepos(org string, user bool) ([]github.Repo, error) 155 ListAppInstallationsForOrg(org string) ([]github.AppInstallation, error) 156 ListCollaborators(org, repo string) ([]github.User, error) 157 ListRepoTeams(org, repo string) ([]github.Team, error) 158 } 159 160 type protector struct { 161 client client 162 cfg *config.Config 163 updates chan requirements 164 errors Errors 165 completedRepos map[string]bool 166 done chan []error 167 verifyRestrictions bool 168 enableAppsRestrictions bool 169 enabled func(org, repo string) bool 170 } 171 172 func (p *protector) configureBranches() { 173 for u := range p.updates { 174 if u.Request == nil { 175 if err := p.client.RemoveBranchProtection(u.Org, u.Repo, u.Branch); err != nil { 176 p.errors.add(fmt.Errorf("remove %s/%s=%s protection failed: %w", u.Org, u.Repo, u.Branch, err)) 177 } 178 continue 179 } 180 181 if err := p.client.UpdateBranchProtection(u.Org, u.Repo, u.Branch, *u.Request); err != nil { 182 p.errors.add(fmt.Errorf("update %s/%s=%s protection to %v failed: %w", u.Org, u.Repo, u.Branch, *u.Request, err)) 183 } 184 } 185 p.done <- p.errors.errs 186 } 187 188 // protect protects branches specified in the presubmit and branch-protection config sections. 189 func (p *protector) protect() { 190 bp := p.cfg.BranchProtection 191 if bp.Policy.Unmanaged != nil && *bp.Policy.Unmanaged && !bp.HasManagedOrgs() && !bp.HasManagedRepos() && !bp.HasManagedBranches() { 192 logrus.Warn("Branchprotection has global unmanaged: true, will not do anything") 193 return 194 } 195 196 // Scan the branch-protection configuration 197 for orgName := range bp.Orgs { 198 if !p.enabled(orgName, "") { 199 continue 200 } 201 org := bp.GetOrg(orgName) 202 if err := p.UpdateOrg(orgName, *org); err != nil { 203 p.errors.add(fmt.Errorf("update %s: %w", orgName, err)) 204 } 205 } 206 207 // Do not automatically protect tested repositories 208 if bp.ProtectTested == nil || !*bp.ProtectTested { 209 return 210 } 211 212 // Some repos with presubmits might not be listed in the branch-protection 213 // Using PresubmitsStatic here is safe because this is only about getting to 214 // know which repos exist. Repos that use in-repo config will appear here, 215 // because we generate a verification job for them 216 for repo := range p.cfg.PresubmitsStatic { 217 if p.completedRepos[repo] { 218 continue 219 } 220 parts := strings.Split(repo, "/") 221 if len(parts) != 2 { // TODO(fejta): use a strong type here instead 222 p.errors.add(fmt.Errorf("bad presubmit repo: %s", repo)) 223 continue 224 } 225 orgName := parts[0] 226 repoName := parts[1] 227 if !p.enabled(orgName, repoName) { 228 continue 229 } 230 repo := bp.GetOrg(orgName).GetRepo(repoName) 231 if err := p.UpdateRepo(orgName, repoName, *repo); err != nil { 232 p.errors.add(fmt.Errorf("update %s/%s: %w", orgName, repoName, err)) 233 } 234 } 235 } 236 237 // UpdateOrg updates all repos in the org with the specified defaults 238 func (p *protector) UpdateOrg(orgName string, org config.Org) error { 239 if org.Policy.Unmanaged != nil && *org.Policy.Unmanaged && !org.HasManagedRepos() && !org.HasManagedBranches() { 240 return nil 241 } 242 243 var repos []string 244 if org.Protect != nil { 245 // Strongly opinionated org, configure every repo in the org. 246 rs, err := p.client.GetRepos(orgName, false) 247 if err != nil { 248 return fmt.Errorf("list repos: %w", err) 249 } 250 for _, r := range rs { 251 // Skip Archived repos as they can't be modified in this way 252 if r.Archived { 253 continue 254 } 255 // Skip private security forks as they can't be modified in this way 256 if r.Private && github.SecurityForkNameRE.MatchString(r.Name) { 257 continue 258 } 259 repos = append(repos, r.Name) 260 } 261 } else { 262 // Unopinionated org, just set explicitly defined repos 263 for r := range org.Repos { 264 repos = append(repos, r) 265 } 266 } 267 268 var errs []error 269 for _, repoName := range repos { 270 if !p.enabled(orgName, repoName) { 271 continue 272 } 273 repo := org.GetRepo(repoName) 274 if err := p.UpdateRepo(orgName, repoName, *repo); err != nil { 275 errs = append(errs, fmt.Errorf("update %s: %w", repoName, err)) 276 } 277 } 278 279 return utilerrors.NewAggregate(errs) 280 } 281 282 // UpdateRepo updates all branches in the repo with the specified defaults 283 func (p *protector) UpdateRepo(orgName string, repoName string, repo config.Repo) error { 284 p.completedRepos[orgName+"/"+repoName] = true 285 if repo.Policy.Unmanaged != nil && *repo.Policy.Unmanaged && !repo.HasManagedBranches() { 286 return nil 287 } 288 289 githubRepo, err := p.client.GetRepo(orgName, repoName) 290 if err != nil { 291 return fmt.Errorf("could not get repo to check for archival: %w", err) 292 } 293 // Skip Archived repos as they can't be modified in this way 294 if githubRepo.Archived { 295 return nil 296 } 297 // Skip private security forks as they can't be modified in this way 298 if githubRepo.Private && github.SecurityForkNameRE.MatchString(githubRepo.Name) { 299 return nil 300 } 301 302 var branchInclusions *regexp.Regexp 303 if len(repo.Policy.Include) > 0 { 304 branchInclusions, err = regexp.Compile(strings.Join(repo.Policy.Include, `|`)) 305 if err != nil { 306 return err 307 } 308 } 309 310 var branchExclusions *regexp.Regexp 311 if len(repo.Policy.Exclude) > 0 { 312 branchExclusions, err = regexp.Compile(strings.Join(repo.Policy.Exclude, `|`)) 313 if err != nil { 314 return err 315 } 316 } 317 318 branches := map[string]github.Branch{} 319 for _, onlyProtected := range []bool{false, true} { // put true second so b.Protected is set correctly 320 bs, err := p.client.GetBranches(orgName, repoName, onlyProtected) 321 if err != nil { 322 return fmt.Errorf("list branches: %w", err) 323 } 324 for _, b := range bs { 325 _, ok := repo.Branches[b.Name] 326 if !ok && branchInclusions != nil && branchInclusions.MatchString(b.Name) { 327 branches[b.Name] = b 328 } else if !ok && branchInclusions != nil && !branchInclusions.MatchString(b.Name) { 329 logrus.Infof("%s/%s=%s: not included", orgName, repoName, b.Name) 330 continue 331 } else if !ok && branchExclusions != nil && branchExclusions.MatchString(b.Name) { 332 logrus.Infof("%s/%s=%s: excluded", orgName, repoName, b.Name) 333 continue 334 } 335 branches[b.Name] = b 336 } 337 } 338 339 var apps, collaborators, teams []string 340 if p.verifyRestrictions { 341 apps, err = p.authorizedApps(orgName) 342 if err != nil { 343 logrus.Infof("%s: error getting list of installed apps: %v", orgName, err) 344 return err 345 } 346 347 collaborators, err = p.authorizedCollaborators(orgName, repoName) 348 if err != nil { 349 logrus.Infof("%s/%s: error getting list of collaborators: %v", orgName, repoName, err) 350 return err 351 } 352 353 teams, err = p.authorizedTeams(orgName, repoName) 354 if err != nil { 355 logrus.Infof("%s/%s: error getting list of teams: %v", orgName, repoName, err) 356 return err 357 } 358 } 359 360 var errs []error 361 for bn, githubBranch := range branches { 362 if branch, err := repo.GetBranch(bn); err != nil { 363 errs = append(errs, fmt.Errorf("get %s: %w", bn, err)) 364 } else if err = p.UpdateBranch(orgName, repoName, bn, *branch, githubBranch.Protected, apps, collaborators, teams); err != nil { 365 errs = append(errs, fmt.Errorf("update %s from protected=%t: %w", bn, githubBranch.Protected, err)) 366 } 367 } 368 369 return utilerrors.NewAggregate(errs) 370 } 371 372 // authorizedApps returns the list of slugs for apps that are authorized 373 // to write to repositories of the org. 374 func (p *protector) authorizedApps(org string) ([]string, error) { 375 appInstallations, err := p.client.ListAppInstallationsForOrg(org) 376 if err != nil { 377 return nil, err 378 } 379 var authorized []string 380 for _, a := range appInstallations { 381 if a.Permissions.Contents == string(github.Write) { 382 authorized = append(authorized, a.AppSlug) 383 } 384 } 385 return authorized, nil 386 } 387 388 // authorizedCollaborators returns the list of Logins for users that are 389 // authorized to write to a repository. 390 func (p *protector) authorizedCollaborators(org, repo string) ([]string, error) { 391 collaborators, err := p.client.ListCollaborators(org, repo) 392 if err != nil { 393 return nil, err 394 } 395 var authorized []string 396 for _, c := range collaborators { 397 if c.Permissions.Admin || c.Permissions.Push { 398 authorized = append(authorized, github.NormLogin(c.Login)) 399 } 400 } 401 return authorized, nil 402 } 403 404 // authorizedTeams returns the list of slugs for teams that are authorized to 405 // write to a repository. 406 func (p *protector) authorizedTeams(org, repo string) ([]string, error) { 407 teams, err := p.client.ListRepoTeams(org, repo) 408 if err != nil { 409 return nil, err 410 } 411 var authorized []string 412 for _, t := range teams { 413 if t.Permission == github.RepoPush || t.Permission == github.RepoAdmin { 414 authorized = append(authorized, t.Slug) 415 } 416 } 417 return authorized, nil 418 } 419 420 func validateRestrictions(org, repo string, bp *github.BranchProtectionRequest, authorizedApps, authorizedCollaborators, authorizedTeams []string) []error { 421 if bp == nil || bp.Restrictions == nil { 422 return nil 423 } 424 425 var errs []error 426 if bp.Restrictions.Apps != nil { 427 if unauthorized := sets.New[string](*bp.Restrictions.Apps...).Difference(sets.New[string](authorizedApps...)); unauthorized.Len() > 0 { 428 errs = append(errs, fmt.Errorf("the following apps are not authorized for %s/%s: %s", org, repo, sets.List(unauthorized))) 429 } 430 } 431 if bp.Restrictions.Users != nil { 432 if unauthorized := sets.New[string](*bp.Restrictions.Users...).Difference(sets.New[string](authorizedCollaborators...)); unauthorized.Len() > 0 { 433 errs = append(errs, fmt.Errorf("the following collaborators are not authorized for %s/%s: %s", org, repo, sets.List(unauthorized))) 434 } 435 } 436 if bp.Restrictions.Teams != nil { 437 if unauthorized := sets.New[string](*bp.Restrictions.Teams...).Difference(sets.New[string](authorizedTeams...)); unauthorized.Len() > 0 { 438 errs = append(errs, fmt.Errorf("the following teams are not authorized for %s/%s: %s", org, repo, sets.List(unauthorized))) 439 } 440 } 441 return errs 442 } 443 444 // UpdateBranch updates the branch with the specified configuration 445 func (p *protector) UpdateBranch(orgName, repo string, branchName string, branch config.Branch, protected bool, authorizedApps, authorizedCollaborators, authorizedTeams []string) error { 446 if branch.Unmanaged != nil && *branch.Unmanaged { 447 return nil 448 } 449 bp, err := p.cfg.GetPolicy(orgName, repo, branchName, branch, p.cfg.GetPresubmitsStatic(orgName+"/"+repo), &protected) 450 if err != nil { 451 return fmt.Errorf("get policy: %w", err) 452 } 453 if bp == nil || bp.Protect == nil { 454 return nil 455 } 456 if !protected && !*bp.Protect { 457 logrus.Infof("%s/%s=%s: already unprotected", orgName, repo, branchName) 458 return nil 459 } 460 461 // Return error if apps restrictions if feature is disabled, but there are apps restrictions in the config 462 if !p.enableAppsRestrictions && bp.Restrictions != nil && bp.Restrictions.Apps != nil { 463 return fmt.Errorf("'enable-apps-restrictions' command line flag is not true, but Apps Restrictions are maintained for %s/%s=%s", orgName, repo, branchName) 464 } 465 466 var req *github.BranchProtectionRequest 467 if *bp.Protect { 468 r := makeRequest(*bp, p.enableAppsRestrictions) 469 req = &r 470 } 471 472 if p.verifyRestrictions { 473 if validationErrors := validateRestrictions(orgName, repo, req, authorizedApps, authorizedCollaborators, authorizedTeams); len(validationErrors) != 0 { 474 logrus.Warnf("invalid branch protection request: %s/%s=%s: %v", orgName, repo, branchName, validationErrors) 475 errs := make([]string, 0, len(validationErrors)) 476 for _, e := range validationErrors { 477 errs = append(errs, e.Error()) 478 } 479 return fmt.Errorf("invalid branch protection request: %s/%s=%s: %s", orgName, repo, branchName, strings.Join(errs, "\n")) 480 } 481 } 482 483 // github API is very sensitive if branchName contains extra characters, 484 // therefor we need to url encode the branch name. 485 branchNameForRequest := url.QueryEscape(branchName) 486 487 // The github API currently does not support listing protections for all 488 // branches of a repository. We therefore have to make individual requests 489 // for each branch. 490 currentBP, err := p.client.GetBranchProtection(orgName, repo, branchNameForRequest) 491 if err != nil { 492 return fmt.Errorf("get current branch protection: %w", err) 493 } 494 495 if equalBranchProtections(currentBP, req) { 496 logrus.Debugf("%s/%s=%s: current branch protection matches policy, skipping", orgName, repo, branchName) 497 return nil 498 } 499 500 p.updates <- requirements{ 501 Org: orgName, 502 Repo: repo, 503 Branch: branchName, 504 Request: req, 505 } 506 return nil 507 } 508 509 func equalBranchProtections(state *github.BranchProtection, request *github.BranchProtectionRequest) bool { 510 switch { 511 case state == nil && request == nil: 512 return true 513 case state != nil && request != nil: 514 return equalRequiredStatusChecks(state.RequiredStatusChecks, request.RequiredStatusChecks) && 515 equalAdminEnforcement(state.EnforceAdmins, request.EnforceAdmins) && 516 equalRequiredPullRequestReviews(state.RequiredPullRequestReviews, request.RequiredPullRequestReviews) && 517 equalRestrictions(state.Restrictions, request.Restrictions) && 518 equalAllowForcePushes(state.AllowForcePushes, request.AllowForcePushes) && 519 equalRequiredLinearHistory(state.RequiredLinearHistory, request.RequiredLinearHistory) && 520 equalAllowDeletions(state.AllowDeletions, request.AllowDeletions) 521 default: 522 return false 523 } 524 } 525 526 func equalRequiredStatusChecks(state, request *github.RequiredStatusChecks) bool { 527 switch { 528 case state == request: 529 return true 530 case state != nil && request != nil: 531 return state.Strict == request.Strict && 532 equalStringSlices(&state.Contexts, &request.Contexts) 533 default: 534 return false 535 } 536 } 537 538 func equalStringSlices(s1, s2 *[]string) bool { 539 switch { 540 case s1 == s2: 541 return true 542 case s1 != nil && s2 != nil: 543 if len(*s1) != len(*s2) { 544 return false 545 } 546 sort.Strings(*s1) 547 sort.Strings(*s2) 548 for i, v := range *s1 { 549 if v != (*s2)[i] { 550 return false 551 } 552 } 553 return true 554 default: 555 return false 556 } 557 } 558 559 func equalRequiredLinearHistory(state github.RequiredLinearHistory, request bool) bool { 560 return state.Enabled == request 561 } 562 563 func equalAllowDeletions(state github.AllowDeletions, request bool) bool { 564 return state.Enabled == request 565 } 566 567 func equalAllowForcePushes(state github.AllowForcePushes, request bool) bool { 568 return state.Enabled == request 569 } 570 571 func equalAdminEnforcement(state github.EnforceAdmins, request *bool) bool { 572 switch { 573 case request == nil: 574 // the state we read from the GitHub API will always contain 575 // a non-nil configuration for admins, while our request may 576 // be nil to signify we do not want to make any statement. 577 // However, not making any statement about admins will buy 578 // into the default behavior, which is for admins to not be 579 // bound by the branch protection rules. Therefore, making no 580 // request is equivalent to making a request to not enforce 581 // rules on admins. 582 return !state.Enabled 583 default: 584 return state.Enabled == *request 585 } 586 } 587 588 func equalRequiredPullRequestReviews(state *github.RequiredPullRequestReviews, request *github.RequiredPullRequestReviewsRequest) bool { 589 switch { 590 case state == nil && request == nil: 591 return true 592 case state != nil && request != nil: 593 return state.DismissStaleReviews == request.DismissStaleReviews && 594 state.RequireCodeOwnerReviews == request.RequireCodeOwnerReviews && 595 state.RequiredApprovingReviewCount == request.RequiredApprovingReviewCount && 596 equalDismissalRestrictions(state.DismissalRestrictions, &request.DismissalRestrictions) && 597 equalBypassRestrictions(state.BypassRestrictions, &request.BypassRestrictions) 598 default: 599 return false 600 } 601 } 602 603 func equalDismissalRestrictions(state *github.DismissalRestrictions, request *github.DismissalRestrictionsRequest) bool { 604 switch { 605 case state == nil && request == nil: 606 return true 607 case state == nil && request != nil: 608 // when there are no restrictions on users or teams, GitHub will 609 // omit the fields from the response we get when asking for the 610 // current state. If we _are_ making a request but it has no real 611 // effect, this is identical to making no request for restriction. 612 return request.Users == nil && request.Teams == nil 613 case state != nil && request != nil: 614 return equalTeams(state.Teams, request.Teams) && equalUsers(state.Users, request.Users) 615 default: 616 return false 617 } 618 } 619 620 func equalBypassRestrictions(state *github.BypassRestrictions, request *github.BypassRestrictionsRequest) bool { 621 switch { 622 case state == nil && request == nil: 623 return true 624 case state == nil && request != nil: 625 // when there are no restrictions on users or teams, GitHub will 626 // omit the fields from the response we get when asking for the 627 // current state. If we _are_ making a request but it has no real 628 // effect, this is identical to making no request for restriction. 629 return request.Users == nil && request.Teams == nil 630 case state != nil && request != nil: 631 return equalTeams(state.Teams, request.Teams) && equalUsers(state.Users, request.Users) 632 default: 633 return false 634 } 635 } 636 637 func equalRestrictions(state *github.Restrictions, request *github.RestrictionsRequest) bool { 638 switch { 639 case state == nil && request == nil: 640 return true 641 case state == nil && request != nil: 642 // when there are no restrictions on apps, users or teams, GitHub will 643 // omit the fields from the response we get when asking for the 644 // current state. If we _are_ making a request but it has no real 645 // effect, this is identical to making no request for restriction. 646 return request.Apps == nil && request.Users == nil && request.Teams == nil 647 case state != nil && request != nil: 648 return equalApps(state.Apps, request.Apps) && equalTeams(state.Teams, request.Teams) && equalUsers(state.Users, request.Users) 649 default: 650 return false 651 } 652 } 653 654 func equalApps(stateApps []github.App, requestApps *[]string) bool { 655 var apps []string 656 for _, app := range stateApps { 657 // RestrictionsRequests record the app by slug, not name 658 apps = append(apps, app.Slug) 659 } 660 // Treat unspecified Apps configuration as no change that we do not create a breaking change when introducing Apps in branchprotector 661 // TODO: could be changed when "enableAppsRestrictions" flag is not needed anymore 662 return equalStringSlices(&apps, requestApps) || requestApps == nil 663 } 664 665 func equalTeams(stateTeams []github.Team, requestTeams *[]string) bool { 666 var teams []string 667 for _, team := range stateTeams { 668 // RestrictionsRequests record the team by slug, not name 669 teams = append(teams, team.Slug) 670 } 671 return equalStringSlices(&teams, requestTeams) 672 } 673 674 func equalUsers(stateUsers []github.User, requestUsers *[]string) bool { 675 var users []string 676 for _, user := range stateUsers { 677 users = append(users, github.NormLogin(user.Login)) 678 } 679 var requestUsersNorm []string 680 if requestUsers != nil { 681 for _, user := range *requestUsers { 682 requestUsersNorm = append(requestUsersNorm, github.NormLogin(user)) 683 } 684 } 685 return equalStringSlices(&users, &requestUsersNorm) 686 }