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  }