go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/git/gitacls/acls_test.go (about)

     1  // Copyright 2018 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 gitacls
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  
    22  	"go.chromium.org/luci/appengine/gaetesting"
    23  	"go.chromium.org/luci/auth/identity"
    24  	"go.chromium.org/luci/config/validation"
    25  	configpb "go.chromium.org/luci/milo/proto/config"
    26  	"go.chromium.org/luci/server/auth"
    27  	"go.chromium.org/luci/server/auth/authtest"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  )
    31  
    32  func TestACLsWork(t *testing.T) {
    33  	t.Parallel()
    34  	c := gaetesting.TestingContext()
    35  
    36  	Convey("ACLs work", t, func() {
    37  		Convey("Validation works", func() {
    38  			validate := func(cfg ...*configpb.Settings_SourceAcls) error {
    39  				ctx := validation.Context{Context: c}
    40  				ctx.SetFile("settings.cfg")
    41  				ValidateConfig(&ctx, cfg)
    42  				return ctx.Finalize()
    43  			}
    44  			mustError := func(cfg ...*configpb.Settings_SourceAcls) multiError {
    45  				err := validate(cfg...)
    46  				So(err, ShouldNotBeNil)
    47  				return multiError(err.(*validation.Error).Errors)
    48  			}
    49  
    50  			valid := configpb.Settings_SourceAcls{
    51  				Hosts:    []string{"a.googlesource.com"},
    52  				Projects: []string{"https://b.googlesource.com/c"},
    53  				Readers:  []string{"group:g", "user@example.com"},
    54  			}
    55  			So(validate(&valid), ShouldBeNil)
    56  
    57  			mustError(&configpb.Settings_SourceAcls{}).with(
    58  				"at least 1 reader required",
    59  				"at least 1 host or project required",
    60  			)
    61  
    62  			Convey("readers", func() {
    63  				mod := valid
    64  				mod.Readers = []string{"bad:kind", "group:", "user", "group:a", "group:a"}
    65  				mustError(&mod).with(
    66  					`invalid readers "bad:kind"`,
    67  					`invalid readers "group:": needs a group name`,
    68  					`invalid readers "user"`,
    69  					`duplicate`,
    70  				)
    71  			})
    72  
    73  			Convey("hosts", func() {
    74  				second := configpb.Settings_SourceAcls{
    75  					Hosts: []string{
    76  						valid.Hosts[0],
    77  						"example.com",
    78  						"repo.googlesource.com/repo",
    79  						"b.googlesource.com", // valid.Project was from here, and it's OK.
    80  					},
    81  					Readers: []string{"group:a"},
    82  				}
    83  				mustError(&valid, &second).with(
    84  					`host "a.googlesource.com"): has already been defined in source_acl #0`,
    85  					`isn't at *.googlesource.com`,
    86  					"shouldn't have path or fragment components",
    87  				)
    88  			})
    89  
    90  			Convey("projects", func() {
    91  				second := configpb.Settings_SourceAcls{
    92  					Hosts: []string{"r.googlesource.com"},
    93  					Projects: []string{
    94  						valid.Projects[0], // dups of prev blocks are OK.
    95  						"r.googlesource.com/redundant",
    96  						"not-repo.googlesource.com",
    97  						"c.googlesource.com/a/repo.git#123",
    98  						"c-review.googlesource.com/src",
    99  						"https://\\meh",
   100  						valid.Projects[0], // dups of projects in this block is not OK.
   101  					},
   102  					Readers: []string{"group:b"},
   103  				}
   104  				mustError(&valid, &second).with(
   105  					`redundant because already covered by its host in the same source_acls block`,
   106  					`project "not-repo.googlesource.com"): should not be just a host`,
   107  					`should not contain '/a' path prefix`,
   108  					`should not contain '.git'`,
   109  					`shouldn't have fragment components`,
   110  					`must not be a Gerrit host (try without '-review')`,
   111  					`not a valid URL`,
   112  					`duplicate, already defined in the same source_acls block`,
   113  				)
   114  			})
   115  		})
   116  
   117  		load := func(cfg ...*configpb.Settings_SourceAcls) *ACLs {
   118  			a, err := FromConfig(c, cfg)
   119  			if err != nil {
   120  				panic(err) // for stacktrace.
   121  			}
   122  			return a
   123  		}
   124  
   125  		Convey("Loading works", func() {
   126  			So(load(
   127  				&configpb.Settings_SourceAcls{
   128  					Hosts: []string{"first.googlesource.com"},
   129  					Projects: []string{
   130  						"second.googlesource.com/y1",
   131  						"third.googlesource.com/z",
   132  					},
   133  					Readers: []string{"user@example.com"},
   134  				}),
   135  				ShouldResemble,
   136  				&ACLs{
   137  					hosts: map[string]*hostACLs{
   138  						"first.googlesource.com": {
   139  							itemACLs: itemACLs{
   140  								definedIn: 0,
   141  								readers:   []string{"user:user@example.com"},
   142  							},
   143  						},
   144  						"second.googlesource.com": {
   145  							itemACLs: itemACLs{definedIn: -1},
   146  							projects: map[string]*itemACLs{
   147  								"y1": {
   148  									definedIn: 0,
   149  									readers:   []string{"user:user@example.com"},
   150  								},
   151  							},
   152  						},
   153  						"third.googlesource.com": {
   154  							itemACLs: itemACLs{definedIn: -1},
   155  							projects: map[string]*itemACLs{
   156  								"z": {
   157  									definedIn: 0,
   158  									readers:   []string{"user:user@example.com"},
   159  								},
   160  							},
   161  						},
   162  					},
   163  				})
   164  
   165  			So(load(
   166  				&configpb.Settings_SourceAcls{
   167  					Hosts: []string{"first.googlesource.com"},
   168  					Projects: []string{
   169  						"second.googlesource.com/y1",
   170  						"third.googlesource.com/z",
   171  					},
   172  					Readers: []string{"user@example.com"},
   173  				},
   174  				&configpb.Settings_SourceAcls{
   175  					Hosts: []string{"third.googlesource.com"},
   176  					Projects: []string{
   177  						"first.googlesource.com/x",
   178  						"second.googlesource.com/y1",
   179  						"second.googlesource.com/y2",
   180  					},
   181  					Readers: []string{"group:g"},
   182  				}),
   183  				ShouldResemble,
   184  				&ACLs{
   185  					hosts: map[string]*hostACLs{
   186  						"first.googlesource.com": {
   187  							itemACLs: itemACLs{
   188  								definedIn: 0,
   189  								readers:   []string{"user:user@example.com"},
   190  							},
   191  							projects: map[string]*itemACLs{
   192  								"x": {
   193  									definedIn: 1,
   194  									readers:   []string{"group:g"},
   195  								},
   196  							},
   197  						},
   198  						"second.googlesource.com": {
   199  							itemACLs: itemACLs{definedIn: -1},
   200  							projects: map[string]*itemACLs{
   201  								"y1": {
   202  									definedIn: 1,
   203  									readers:   []string{"group:g", "user:user@example.com"},
   204  								},
   205  								"y2": {
   206  									definedIn: 1,
   207  									readers:   []string{"group:g"},
   208  								},
   209  							},
   210  						},
   211  						"third.googlesource.com": {
   212  							itemACLs: itemACLs{
   213  								definedIn: 1,
   214  								readers:   []string{"group:g"},
   215  							},
   216  							projects: map[string]*itemACLs{
   217  								"z": {
   218  									definedIn: 0,
   219  									readers:   []string{"user:user@example.com"},
   220  								},
   221  							},
   222  						},
   223  					},
   224  				})
   225  		})
   226  
   227  		Convey("IsAllowed works", func() {
   228  			acls := load(
   229  				&configpb.Settings_SourceAcls{
   230  					Hosts:    []string{"public.googlesource.com"},
   231  					Projects: []string{"limited.googlesource.com/public"},
   232  					Readers:  []string{"group:all"},
   233  				},
   234  				&configpb.Settings_SourceAcls{
   235  					Hosts:    []string{"limited.googlesource.com"},
   236  					Projects: []string{"c.googlesource.com/private"},
   237  					Readers:  []string{"group:some", "they@example.com"},
   238  				},
   239  			)
   240  			granted := func(ctx context.Context, host, project string) bool {
   241  				r, err := acls.IsAllowed(ctx, host, project)
   242  				if err != nil {
   243  					panic(err)
   244  				}
   245  				return r
   246  			}
   247  
   248  			cAnon := auth.WithState(c, &authtest.FakeState{
   249  				Identity:       identity.AnonymousIdentity,
   250  				IdentityGroups: []string{"all"},
   251  			})
   252  			So(granted(cAnon, "public.googlesource.com", "any"), ShouldBeTrue)
   253  			So(granted(cAnon, "limited.googlesource.com", "public"), ShouldBeTrue)
   254  			So(granted(cAnon, "limited.googlesource.com", "any"), ShouldBeFalse)
   255  
   256  			cThey := auth.WithState(c, &authtest.FakeState{
   257  				Identity:       "user:they@example.com",
   258  				IdentityGroups: []string{"all"},
   259  			})
   260  			So(granted(cThey, "limited.googlesource.com", "public"), ShouldBeTrue)
   261  			So(granted(cThey, "limited.googlesource.com", "any"), ShouldBeTrue)
   262  			So(granted(cThey, "c.googlesource.com", "private"), ShouldBeTrue)
   263  			So(granted(cThey, "c.googlesource.com", "nope"), ShouldBeFalse)
   264  
   265  			cSome := auth.WithState(c, &authtest.FakeState{
   266  				Identity:       "user:some@example.com",
   267  				IdentityGroups: []string{"some", "all"},
   268  			})
   269  			So(granted(cSome, "limited.googlesource.com", "any"), ShouldBeTrue)
   270  			So(granted(cSome, "c.googlesource.com", "private"), ShouldBeTrue)
   271  			So(granted(cSome, "c.googlesource.com", "nope"), ShouldBeFalse)
   272  
   273  			Convey("for Gerrit, too", func() {
   274  				So(granted(cAnon, "public-review.googlesource.com", "any"), ShouldBeTrue)
   275  				So(granted(cThey, "limited-review.googlesource.com", "any"), ShouldBeTrue)
   276  				So(granted(cSome, "c-review.googlesource.com", "private"), ShouldBeTrue)
   277  			})
   278  		})
   279  	})
   280  }
   281  
   282  type multiError []error
   283  
   284  func (m multiError) with(substrings ...string) {
   285  	for i, err := range m {
   286  		if i >= len(substrings) {
   287  			So(fmt.Errorf("extra errors produced: %q", m[i:]), ShouldBeNil)
   288  		} else {
   289  			So(err.Error(), ShouldContainSubstring, substrings[i])
   290  		}
   291  	}
   292  	if len(substrings) > len(m) {
   293  		panic(fmt.Errorf("not produced errors: %q", substrings[len(m):]))
   294  	}
   295  }