blob: 548921689bf00c9b7bf6ebb37e1d3242f51e4735 [file] [log] [blame]
// Copyright 2015 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 auth
import (
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"golang.org/x/oauth2"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/realms"
"go.chromium.org/luci/server/auth/service/protocol"
"go.chromium.org/luci/server/auth/signing"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestAuthenticate(t *testing.T) {
t.Parallel()
Convey("Happy path", t, func() {
c := injectTestDB(context.Background(), &fakeDB{
allowedClientID: "some_client_id",
})
auth := Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "some_client_id"}},
}
req := makeRequest()
req.RemoteAddr = "1.2.3.4"
c, err := auth.Authenticate(c, req)
So(err, ShouldBeNil)
So(CurrentUser(c), ShouldResemble, &User{
Identity: "user:abc@example.com",
Email: "abc@example.com",
ClientID: "some_client_id",
})
So(GetState(c).PeerIP().String(), ShouldEqual, "1.2.3.4")
url, err := LoginURL(c, "login")
So(err, ShouldBeNil)
So(url, ShouldEqual, "http://fake.login.url/login")
url, err = LogoutURL(c, "logout")
So(err, ShouldBeNil)
So(url, ShouldEqual, "http://fake.logout.url/logout")
tok, extra, err := GetState(c).UserCredentials()
So(err, ShouldBeNil)
So(tok, ShouldResemble, &oauth2.Token{AccessToken: "token-abc@example.com"})
So(extra, ShouldHaveLength, 0)
})
Convey("Custom EndUserIP implementation", t, func() {
req := makeRequest()
req.Header.Add("X-Custom-IP", "4.5.6.7")
c := injectTestDB(context.Background(), &fakeDB{})
c = ModifyConfig(c, func(cfg Config) Config {
cfg.EndUserIP = func(r *http.Request) string { return r.Header.Get("X-Custom-IP") }
return cfg
})
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "zzz@example.com"}},
}
c, err := auth.Authenticate(c, req)
So(err, ShouldBeNil)
So(GetState(c).PeerIP().String(), ShouldEqual, "4.5.6.7")
})
Convey("No methods given", t, func() {
c := injectTestDB(context.Background(), &fakeDB{
allowedClientID: "some_client_id",
})
auth := Authenticator{}
_, err := auth.Authenticate(c, makeRequest())
So(err, ShouldEqual, ErrNotConfigured)
})
Convey("IsAllowedOAuthClientID on default DB", t, func() {
c := context.Background()
auth := Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "some_client_id"}},
}
_, err := auth.Authenticate(c, makeRequest())
So(err, ShouldErrLike, "the library is not properly configured")
})
Convey("IsAllowedOAuthClientID with invalid client_id", t, func() {
c := injectTestDB(context.Background(), &fakeDB{
allowedClientID: "some_client_id",
})
c = injectFrontendClientID(c, "frontend_client_id")
auth := Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "another_client_id"}},
}
_, err := auth.Authenticate(c, makeRequest())
So(err, ShouldEqual, ErrBadClientID)
})
Convey("IsAllowedOAuthClientID with frontend client_id", t, func() {
c := injectTestDB(context.Background(), &fakeDB{
allowedClientID: "some_client_id",
})
c = injectFrontendClientID(c, "frontend_client_id")
auth := Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "frontend_client_id"}},
}
_, err := auth.Authenticate(c, makeRequest())
So(err, ShouldBeNil) // success!
})
Convey("IP whitelist restriction works", t, func() {
db, err := authdb.NewSnapshotDB(&protocol.AuthDB{
IpWhitelistAssignments: []*protocol.AuthIPWhitelistAssignment{
{
Identity: "user:abc@example.com",
IpWhitelist: "whitelist",
},
},
IpWhitelists: []*protocol.AuthIPWhitelist{
{
Name: "whitelist",
Subnets: []string{
"1.2.3.4/32",
},
},
},
}, "http://auth-service", 1234, false)
So(err, ShouldBeNil)
c := injectTestDB(context.Background(), db)
Convey("User is using IP whitelist and IP is in the whitelist.", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "abc@example.com"}},
}
req := makeRequest()
req.RemoteAddr = "1.2.3.4"
c, err := auth.Authenticate(c, req)
So(err, ShouldBeNil)
So(CurrentIdentity(c), ShouldEqual, identity.Identity("user:abc@example.com"))
})
Convey("User is using IP whitelist and IP is NOT in the whitelist.", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "abc@example.com"}},
}
req := makeRequest()
req.RemoteAddr = "1.2.3.5"
_, err := auth.Authenticate(c, req)
So(err, ShouldEqual, ErrIPNotWhitelisted)
})
Convey("User is not using IP whitelist.", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "def@example.com"}},
}
req := makeRequest()
req.RemoteAddr = "1.2.3.5"
c, err := auth.Authenticate(c, req)
So(err, ShouldBeNil)
So(CurrentIdentity(c), ShouldEqual, identity.Identity("user:def@example.com"))
})
})
Convey("X-Luci-Project works", t, func() {
c := injectTestDB(context.Background(), &fakeDB{
groups: map[string][]identity.Identity{
InternalServicesGroup: {"user:allowed@example.com"},
},
})
Convey("Allowed", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "allowed@example.com"}},
}
req := makeRequest()
req.Header.Set(XLUCIProjectHeader, "test-proj")
c, err := auth.Authenticate(c, req)
So(err, ShouldBeNil)
So(CurrentIdentity(c), ShouldEqual, identity.Identity("project:test-proj"))
tok, extra, err := GetState(c).UserCredentials()
So(err, ShouldBeNil)
So(tok, ShouldResemble, &oauth2.Token{AccessToken: "token-allowed@example.com"})
So(extra, ShouldResemble, map[string]string{XLUCIProjectHeader: "test-proj"})
})
Convey("Forbidden", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "unknown@example.com"}},
}
req := makeRequest()
req.Header.Set(XLUCIProjectHeader, "test-proj")
_, err := auth.Authenticate(c, req)
So(err, ShouldEqual, ErrProjectHeaderForbidden)
})
Convey("Bad project ID", func() {
auth := Authenticator{
Methods: []Method{fakeAuthMethod{email: "allowed@example.com"}},
}
req := makeRequest()
req.Header.Set(XLUCIProjectHeader, "?????")
_, err := auth.Authenticate(c, req)
So(err, ShouldErrLike, "bad value")
})
})
}
func TestMiddleware(t *testing.T) {
t.Parallel()
handler := func(c *router.Context) {
fmt.Fprintf(c.Writer, "%s", CurrentIdentity(c.Context))
}
call := func(a *Authenticator) *httptest.ResponseRecorder {
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
So(err, ShouldBeNil)
w := httptest.NewRecorder()
router.RunMiddleware(&router.Context{
Context: injectTestDB(context.Background(), &fakeDB{
allowedClientID: "some_client_id",
}),
Writer: w,
Request: req,
}, router.NewMiddlewareChain(a.GetMiddleware()), handler)
return w
}
Convey("Happy path", t, func() {
rr := call(&Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "some_client_id"}},
})
So(rr.Code, ShouldEqual, 200)
So(rr.Body.String(), ShouldEqual, "user:abc@example.com")
})
Convey("Fatal error", t, func() {
rr := call(&Authenticator{
Methods: []Method{fakeAuthMethod{clientID: "another_client_id"}},
})
So(rr.Code, ShouldEqual, 403)
So(rr.Body.String(), ShouldEqual, ErrBadClientID.Error()+"\n")
})
Convey("Transient error", t, func() {
rr := call(&Authenticator{
Methods: []Method{fakeAuthMethod{err: errors.New("boo", transient.Tag)}},
})
So(rr.Code, ShouldEqual, 500)
So(rr.Body.String(), ShouldEqual, "Internal Server Error\n")
})
}
///
func makeRequest() *http.Request {
req, _ := http.NewRequest("GET", "http://some-url", nil)
return req
}
///
// fakeAuthMethod implements Method.
type fakeAuthMethod struct {
err error
clientID string
email string
}
func (m fakeAuthMethod) Authenticate(context.Context, *http.Request) (*User, Session, error) {
if m.err != nil {
return nil, nil, m.err
}
email := m.email
if email == "" {
email = "abc@example.com"
}
return &User{
Identity: identity.Identity("user:" + email),
Email: email,
ClientID: m.clientID,
}, nil, nil
}
func (m fakeAuthMethod) LoginURL(ctx context.Context, dest string) (string, error) {
return "http://fake.login.url/" + dest, nil
}
func (m fakeAuthMethod) LogoutURL(ctx context.Context, dest string) (string, error) {
return "http://fake.logout.url/" + dest, nil
}
func (m fakeAuthMethod) GetUserCredentials(context.Context, *http.Request) (*oauth2.Token, error) {
email := m.email
if email == "" {
email = "abc@example.com"
}
return &oauth2.Token{AccessToken: "token-" + email}, nil
}
func injectTestDB(ctx context.Context, d authdb.DB) context.Context {
return ModifyConfig(ctx, func(cfg Config) Config {
cfg.DBProvider = func(ctx context.Context) (authdb.DB, error) {
return d, nil
}
return cfg
})
}
func injectFrontendClientID(ctx context.Context, clientID string) context.Context {
return ModifyConfig(ctx, func(cfg Config) Config {
cfg.FrontendClientID = func(context.Context) (string, error) {
return clientID, nil
}
return cfg
})
}
///
// fakeDB implements DB.
type fakeDB struct {
allowedClientID string
internalService string
authServiceURL string
tokenServiceURL string
groups map[string][]identity.Identity
realmData map[string]*protocol.RealmData
}
func (db *fakeDB) IsAllowedOAuthClientID(ctx context.Context, email, clientID string) (bool, error) {
return clientID == db.allowedClientID, nil
}
func (db *fakeDB) IsInternalService(ctx context.Context, hostname string) (bool, error) {
return hostname == db.internalService, nil
}
func (db *fakeDB) IsMember(ctx context.Context, id identity.Identity, groups []string) (bool, error) {
for _, g := range groups {
for _, member := range db.groups[g] {
if id == member {
return true, nil
}
}
}
return false, nil
}
func (db *fakeDB) CheckMembership(ctx context.Context, id identity.Identity, groups []string) ([]string, error) {
panic("not implemented")
}
func (db *fakeDB) HasPermission(ctx context.Context, id identity.Identity, perm realms.Permission, realm string, attrs realms.Attrs) (bool, error) {
return false, errors.New("fakeDB: HasPermission is not implemented")
}
func (db *fakeDB) QueryRealms(ctx context.Context, id identity.Identity, perm realms.Permission, project string, attrs realms.Attrs) ([]string, error) {
return nil, errors.New("fakeDB: QueryRealms is not implemented")
}
func (db *fakeDB) GetCertificates(ctx context.Context, id identity.Identity) (*signing.PublicCertificates, error) {
return nil, errors.New("fakeDB: GetCertificates is not implemented")
}
func (db *fakeDB) GetWhitelistForIdentity(ctx context.Context, ident identity.Identity) (string, error) {
return "", nil
}
func (db *fakeDB) IsInWhitelist(ctx context.Context, ip net.IP, whitelist string) (bool, error) {
return whitelist == "bots" && ip.String() == "1.2.3.4", nil
}
func (db *fakeDB) GetAuthServiceURL(ctx context.Context) (string, error) {
if db.authServiceURL == "" {
return "", errors.New("fakeDB: GetAuthServiceURL is not configured")
}
return db.authServiceURL, nil
}
func (db *fakeDB) GetTokenServiceURL(ctx context.Context) (string, error) {
if db.tokenServiceURL == "" {
return "", errors.New("fakeDB: GetTokenServiceURL is not configured")
}
return db.tokenServiceURL, nil
}
func (db *fakeDB) GetRealmData(ctx context.Context, realm string) (*protocol.RealmData, error) {
return db.realmData[realm], nil
}