blob: 08aebb3ad612d7fb11f90c6be667b59b60dc3ff9 [file] [log] [blame]
// Copyright 2018 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 gitacls
import (
"context"
"fmt"
"testing"
"go.chromium.org/luci/appengine/gaetesting"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/config/validation"
"go.chromium.org/luci/milo/api/config"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
. "github.com/smartystreets/goconvey/convey"
)
func TestACLsWork(t *testing.T) {
t.Parallel()
c := gaetesting.TestingContext()
Convey("ACLs work", t, func() {
Convey("Validation works", func() {
validate := func(cfg ...*config.Settings_SourceAcls) error {
ctx := validation.Context{Context: c}
ctx.SetFile("settings.cfg")
ValidateConfig(&ctx, cfg)
return ctx.Finalize()
}
mustError := func(cfg ...*config.Settings_SourceAcls) multiError {
err := validate(cfg...)
So(err, ShouldNotBeNil)
return multiError(err.(*validation.Error).Errors)
}
valid := config.Settings_SourceAcls{
Hosts: []string{"a.googlesource.com"},
Projects: []string{"https://b.googlesource.com/c"},
Readers: []string{"group:g", "user@example.com"},
}
So(validate(&valid), ShouldBeNil)
mustError(&config.Settings_SourceAcls{}).with(
"at least 1 reader required",
"at least 1 host or project required",
)
Convey("readers", func() {
mod := valid
mod.Readers = []string{"bad:kind", "group:", "user", "group:a", "group:a"}
mustError(&mod).with(
`invalid readers "bad:kind"`,
`invalid readers "group:": needs a group name`,
`invalid readers "user"`,
`duplicate`,
)
})
Convey("hosts", func() {
second := config.Settings_SourceAcls{
Hosts: []string{
valid.Hosts[0],
"example.com",
"repo.googlesource.com/repo",
"b.googlesource.com", // valid.Project was from here, and it's OK.
},
Readers: []string{"group:a"},
}
mustError(&valid, &second).with(
`host "a.googlesource.com"): has already been defined in source_acl #0`,
`isn't at *.googlesource.com`,
"shouldn't have path or fragment components",
)
})
Convey("projects", func() {
second := config.Settings_SourceAcls{
Hosts: []string{"r.googlesource.com"},
Projects: []string{
valid.Projects[0], // dups of prev blocks are OK.
"r.googlesource.com/redundant",
"not-repo.googlesource.com",
"c.googlesource.com/a/repo.git#123",
"c-review.googlesource.com/src",
"https://\\meh",
valid.Projects[0], // dups of projects in this block is not OK.
},
Readers: []string{"group:b"},
}
mustError(&valid, &second).with(
`redundant because already covered by its host in the same source_acls block`,
`project "not-repo.googlesource.com"): should not be just a host`,
`should not contain '/a' path prefix`,
`should not contain '.git'`,
`shouldn't have fragment components`,
`must not be a Gerrit host (try without '-review')`,
`not a valid URL`,
`duplicate, already defined in the same source_acls block`,
)
})
})
load := func(cfg ...*config.Settings_SourceAcls) *ACLs {
a, err := FromConfig(c, cfg)
if err != nil {
panic(err) // for stacktrace.
}
return a
}
Convey("Loading works", func() {
So(load(
&config.Settings_SourceAcls{
Hosts: []string{"first.googlesource.com"},
Projects: []string{
"second.googlesource.com/y1",
"third.googlesource.com/z",
},
Readers: []string{"user@example.com"},
}),
ShouldResemble,
&ACLs{
hosts: map[string]*hostACLs{
"first.googlesource.com": {
itemACLs: itemACLs{
definedIn: 0,
readers: []string{"user:user@example.com"},
},
},
"second.googlesource.com": {
itemACLs: itemACLs{definedIn: -1},
projects: map[string]*itemACLs{
"y1": {
definedIn: 0,
readers: []string{"user:user@example.com"},
},
},
},
"third.googlesource.com": {
itemACLs: itemACLs{definedIn: -1},
projects: map[string]*itemACLs{
"z": {
definedIn: 0,
readers: []string{"user:user@example.com"},
},
},
},
},
})
So(load(
&config.Settings_SourceAcls{
Hosts: []string{"first.googlesource.com"},
Projects: []string{
"second.googlesource.com/y1",
"third.googlesource.com/z",
},
Readers: []string{"user@example.com"},
},
&config.Settings_SourceAcls{
Hosts: []string{"third.googlesource.com"},
Projects: []string{
"first.googlesource.com/x",
"second.googlesource.com/y1",
"second.googlesource.com/y2",
},
Readers: []string{"group:g"},
}),
ShouldResemble,
&ACLs{
hosts: map[string]*hostACLs{
"first.googlesource.com": {
itemACLs: itemACLs{
definedIn: 0,
readers: []string{"user:user@example.com"},
},
projects: map[string]*itemACLs{
"x": {
definedIn: 1,
readers: []string{"group:g"},
},
},
},
"second.googlesource.com": {
itemACLs: itemACLs{definedIn: -1},
projects: map[string]*itemACLs{
"y1": {
definedIn: 1,
readers: []string{"group:g", "user:user@example.com"},
},
"y2": {
definedIn: 1,
readers: []string{"group:g"},
},
},
},
"third.googlesource.com": {
itemACLs: itemACLs{
definedIn: 1,
readers: []string{"group:g"},
},
projects: map[string]*itemACLs{
"z": {
definedIn: 0,
readers: []string{"user:user@example.com"},
},
},
},
},
})
})
Convey("IsAllowed works", func() {
acls := load(
&config.Settings_SourceAcls{
Hosts: []string{"public.googlesource.com"},
Projects: []string{"limited.googlesource.com/public"},
Readers: []string{"group:all"},
},
&config.Settings_SourceAcls{
Hosts: []string{"limited.googlesource.com"},
Projects: []string{"c.googlesource.com/private"},
Readers: []string{"group:some", "they@example.com"},
},
)
granted := func(ctx context.Context, host, project string) bool {
r, err := acls.IsAllowed(ctx, host, project)
if err != nil {
panic(err)
}
return r
}
cAnon := auth.WithState(c, &authtest.FakeState{
Identity: identity.AnonymousIdentity,
IdentityGroups: []string{"all"},
})
So(granted(cAnon, "public.googlesource.com", "any"), ShouldBeTrue)
So(granted(cAnon, "limited.googlesource.com", "public"), ShouldBeTrue)
So(granted(cAnon, "limited.googlesource.com", "any"), ShouldBeFalse)
cThey := auth.WithState(c, &authtest.FakeState{
Identity: "user:they@example.com",
IdentityGroups: []string{"all"},
})
So(granted(cThey, "limited.googlesource.com", "public"), ShouldBeTrue)
So(granted(cThey, "limited.googlesource.com", "any"), ShouldBeTrue)
So(granted(cThey, "c.googlesource.com", "private"), ShouldBeTrue)
So(granted(cThey, "c.googlesource.com", "nope"), ShouldBeFalse)
cSome := auth.WithState(c, &authtest.FakeState{
Identity: "user:some@example.com",
IdentityGroups: []string{"some", "all"},
})
So(granted(cSome, "limited.googlesource.com", "any"), ShouldBeTrue)
So(granted(cSome, "c.googlesource.com", "private"), ShouldBeTrue)
So(granted(cSome, "c.googlesource.com", "nope"), ShouldBeFalse)
Convey("for Gerrit, too", func() {
So(granted(cAnon, "public-review.googlesource.com", "any"), ShouldBeTrue)
So(granted(cThey, "limited-review.googlesource.com", "any"), ShouldBeTrue)
So(granted(cSome, "c-review.googlesource.com", "private"), ShouldBeTrue)
})
})
})
}
type multiError []error
func (m multiError) with(substrings ...string) {
for i, err := range m {
if i >= len(substrings) {
So(fmt.Errorf("extra errors produced: %q", m[i:]), ShouldBeNil)
} else {
So(err.Error(), ShouldContainSubstring, substrings[i])
}
}
if len(substrings) > len(m) {
panic(fmt.Errorf("not produced errors: %q", substrings[len(m):]))
}
}