go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/changelist/access.go (about) 1 // Copyright 2021 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package changelist 16 17 import ( 18 "context" 19 "fmt" 20 21 "go.chromium.org/luci/common/clock" 22 ) 23 24 // AccessKind is the level of access a LUCI project has to a CL. 25 type AccessKind int 26 27 const ( 28 // AccessUnknown means a CL needs refreshing in the context of this project 29 // in order to ascertain the AccessKind. 30 AccessUnknown AccessKind = iota 31 // AccessGranted means this LUCI project has exclusive access to the CL. 32 // 33 // * this LUCI project is configured to watch this config, 34 // * and no other project is; 35 // * this LUCI project has access to the CL in code review (e.g., Gerrit); 36 AccessGranted 37 // AccessDeniedProbably means there is early evidence that LUCI project lacks 38 // access to the project. 39 // 40 // This is a mitigation to Gerrit eventual consistency, which may result in 41 // HTTP 404 returned for a CL that has just been created. 42 AccessDeniedProbably 43 // AccessDenied means the LUCI project has no access to this CL. 44 // 45 // Can be either due to project config not being the only watcher of the CL, 46 // or due to the inability to fetch CL from code review (e.g. Gerrit). 47 AccessDenied 48 ) 49 50 // AccessKind returns AccessKind of a CL. 51 func (cl *CL) AccessKind(ctx context.Context, luciProject string) AccessKind { 52 kind, _ := cl.AccessKindWithReason(ctx, luciProject) 53 return kind 54 } 55 56 // AccessKind returns AccessKind of a CL from code review site. 57 func (cl *CL) AccessKindFromCodeReviewSite(ctx context.Context, luciProject string) AccessKind { 58 if pa := cl.Access.GetByProject()[luciProject]; pa != nil { 59 switch ct, now := pa.GetNoAccessTime(), clock.Now(ctx); { 60 case ct == nil && pa.GetNoAccess(): 61 // Legacy not yet upgraded entity. 62 return AccessDenied 63 case ct == nil: 64 panic(fmt.Errorf("Access.Project %q without NoAccess fields: %s", luciProject, pa)) 65 case now.Before(ct.AsTime()): 66 return AccessDeniedProbably 67 default: 68 return AccessDenied 69 } 70 } 71 return AccessGranted 72 } 73 74 // IsWatchedByThisAndOtherProjects checks if CL is watched by several projects, 75 // one of which is given. 76 // If so, returns the config applicable to the given project and true. 77 // Else, returns nil, false. 78 func (cl *CL) IsWatchedByThisAndOtherProjects(thisProject string) (*ApplicableConfig_Project, bool) { 79 if len(cl.ApplicableConfig.GetProjects()) <= 1 { 80 return nil, false 81 } 82 for _, p := range cl.ApplicableConfig.GetProjects() { 83 if p.GetName() == thisProject { 84 return p, true 85 } 86 } 87 return nil, false 88 } 89 90 // AccessKindWithReason returns AccessKind of a CL and a reason for it. 91 func (cl *CL) AccessKindWithReason(ctx context.Context, luciProject string) (AccessKind, string) { 92 switch projects := cl.ApplicableConfig.GetProjects(); { 93 case cl.ApplicableConfig == nil: 94 // ApplicableConfig may not be always computable w/o first fetching CL from 95 // code review, so this case is handled below. 96 case len(projects) == 0: 97 return AccessDenied, "not watched by any LUCI Project" 98 case len(projects) > 1: 99 return AccessDenied, fmt.Sprintf("watched not only by LUCI Project %q", luciProject) 100 case projects[0].GetName() != luciProject: 101 return AccessDenied, fmt.Sprintf("not watched by LUCI Project %q", luciProject) 102 default: 103 // CL is watched by this project only. 104 } 105 106 switch cl.AccessKindFromCodeReviewSite(ctx, luciProject) { 107 case AccessDenied: 108 return AccessDenied, "code review site denied access" 109 case AccessDeniedProbably: 110 return AccessDeniedProbably, "code review site denied access recently" 111 } 112 113 if cl.ApplicableConfig == nil || cl.Snapshot == nil { 114 return AccessUnknown, "needs a fetch from code review" 115 } 116 if cl.Snapshot.GetLuciProject() != luciProject { 117 return AccessUnknown, "needs a fetch from code review due to Snapshot from old project" 118 } 119 return AccessGranted, "granted" 120 } 121 122 // HasOnlyProject returns true iff ApplicableConfig contains only the given 123 // project, regardless of the number of applicable config groups it may contain. 124 func (a *ApplicableConfig) HasOnlyProject(luciProject string) bool { 125 projects := a.GetProjects() 126 if len(projects) != 1 { 127 return false 128 } 129 return projects[0].GetName() == luciProject 130 } 131 132 // HasProject returns true whether ApplicableConfig contains the given 133 // project, possibly among other projects. 134 func (a *ApplicableConfig) HasProject(luciProject string) bool { 135 for _, p := range a.GetProjects() { 136 if p.Name == luciProject { 137 return true 138 } 139 } 140 return false 141 } 142 143 // SemanticallyEqual checks if ApplicableConfig configs are the same. 144 func (a *ApplicableConfig) SemanticallyEqual(b *ApplicableConfig) bool { 145 if len(a.GetProjects()) != len(b.GetProjects()) { 146 return false 147 } 148 for i, pa := range a.GetProjects() { 149 switch pb := b.GetProjects()[i]; { 150 case pa.GetName() != pb.GetName(): 151 return false 152 case len(pa.GetConfigGroupIds()) != len(pb.GetConfigGroupIds()): 153 return false 154 default: 155 for j, sa := range pa.GetConfigGroupIds() { 156 if sa != pb.GetConfigGroupIds()[j] { 157 return false 158 } 159 } 160 } 161 } 162 return true 163 }