blob: 8b56e8cee6bbce61a3db08e4c74c8c1286682ee9 [file] [log] [blame]
// Copyright 2021 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package changelist
import (
"context"
"fmt"
"go.chromium.org/luci/common/clock"
)
// AccessKind is the level of access a LUCI project has to a CL.
type AccessKind int
const (
// AccessUnknown means a CL needs refreshing in the context of this project
// in order to ascertain the AccessKind.
AccessUnknown AccessKind = iota
// AccessGranted means this LUCI project has exclusive access to the CL.
//
// * this LUCI project is configured to watch this config,
// * and no other project is;
// * this LUCI project has access to the CL in code review (e.g., Gerrit);
AccessGranted
// AccessDeniedProbably means there is early evidence that LUCI project lacks
// access to the project.
//
// This is a mitigation to Gerrit eventual consistency, which may result in
// HTTP 404 returned for a CL that has just been created.
AccessDeniedProbably
// AccessDenied means the LUCI project has no access to this CL.
//
// Can be either due to project config not being the only watcher of the CL,
// or due to the inability to fetch CL from code review (e.g. Gerrit).
AccessDenied
)
// AccessKind returns AccessKind of a CL.
func (cl *CL) AccessKind(ctx context.Context, luciProject string) AccessKind {
kind, _ := cl.AccessKindWithReason(ctx, luciProject)
return kind
}
// AccessKind returns AccessKind of a CL from code review site.
func (cl *CL) AccessKindFromCodeReviewSite(ctx context.Context, luciProject string) AccessKind {
if pa := cl.Access.GetByProject()[luciProject]; pa != nil {
switch ct, now := pa.GetNoAccessTime(), clock.Now(ctx); {
case ct == nil && pa.GetNoAccess():
// Legacy not yet upgraded entity.
return AccessDenied
case ct == nil:
panic(fmt.Errorf("Access.Project %q without NoAccess fields: %s", luciProject, pa))
case now.Before(ct.AsTime()):
return AccessDeniedProbably
default:
return AccessDenied
}
}
return AccessGranted
}
// IsWatchedByThisAndOtherProjects checks if CL is watched by several projects,
// one of which is given.
// If so, returns the config applicable to the given project and true.
// Else, returns nil, false.
func (cl *CL) IsWatchedByThisAndOtherProjects(thisProject string) (*ApplicableConfig_Project, bool) {
if len(cl.ApplicableConfig.GetProjects()) <= 1 {
return nil, false
}
for _, p := range cl.ApplicableConfig.GetProjects() {
if p.GetName() == thisProject {
return p, true
}
}
return nil, false
}
// AccessKindWithReason returns AccessKind of a CL and a reason for it.
func (cl *CL) AccessKindWithReason(ctx context.Context, luciProject string) (AccessKind, string) {
switch projects := cl.ApplicableConfig.GetProjects(); {
case cl.ApplicableConfig == nil:
// ApplicableConfig may not be always computable w/o first fetching CL from
// code review, so this case is handled below.
case len(projects) == 0:
return AccessDenied, "not watched by any LUCI Project"
case len(projects) > 1:
return AccessDenied, fmt.Sprintf("watched not only by LUCI Project %q", luciProject)
case projects[0].GetName() != luciProject:
return AccessDenied, fmt.Sprintf("not watched by LUCI Project %q", luciProject)
default:
// CL is watched by this project only.
}
switch cl.AccessKindFromCodeReviewSite(ctx, luciProject) {
case AccessDenied:
return AccessDenied, "code review site denied access"
case AccessDeniedProbably:
return AccessDeniedProbably, "code review site denied access recently"
}
if cl.ApplicableConfig == nil || cl.Snapshot == nil {
return AccessUnknown, "needs a fetch from code review"
}
if cl.Snapshot.GetLuciProject() != luciProject {
return AccessUnknown, "needs a fetch from code review due to Snapshot from old project"
}
return AccessGranted, "granted"
}
// HasOnlyProject returns true iff ApplicableConfig contains only the given
// project, regardless of the number of applicable config groups it may contain.
func (a *ApplicableConfig) HasOnlyProject(luciProject string) bool {
projects := a.GetProjects()
if len(projects) != 1 {
return false
}
return projects[0].GetName() == luciProject
}
// HasProject returns true whether ApplicableConfig contains the given
// project, possibly among other projects.
func (a *ApplicableConfig) HasProject(luciProject string) bool {
for _, p := range a.GetProjects() {
if p.Name == luciProject {
return true
}
}
return false
}
// SemanticallyEqual checks if ApplicableConfig configs are the same.
func (a *ApplicableConfig) SemanticallyEqual(b *ApplicableConfig) bool {
if len(a.GetProjects()) != len(b.GetProjects()) {
return false
}
for i, pa := range a.GetProjects() {
switch pb := b.GetProjects()[i]; {
case pa.GetName() != pb.GetName():
return false
case len(pa.GetConfigGroupIds()) != len(pb.GetConfigGroupIds()):
return false
default:
for j, sa := range pa.GetConfigGroupIds() {
if sa != pb.GetConfigGroupIds()[j] {
return false
}
}
}
}
return true
}