blob: 6981f42091e9d6971823adadac713f81c09df441 [file] [log] [blame]
package authproxy
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/allowed"
"go.skia.org/infra/go/cleanup"
"go.skia.org/infra/go/mockhttpclient"
"go.skia.org/infra/go/roles"
"go.skia.org/infra/kube/go/authproxy/auth/mocks"
)
const (
viewerEmail = "nobody@example.org"
notAViewerEmail = "notallowed@example.org"
)
var commonAllowed = map[roles.Role]allowed.Allow{
roles.Viewer: allowed.NewAllowedFromList([]string{viewerEmail}),
}
func assertValidEmailAndRole(t *testing.T) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{viewerEmail}, r.Header.Values(WebAuthHeaderName))
require.Equal(t, []string{string(roles.Viewer)}, r.Header.Values(WebAuthRoleHeaderName))
}
}
func setupForTest(t *testing.T, cb http.HandlerFunc) (*url.URL, *bool, *httptest.ResponseRecorder, *http.Request) {
called := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cb(w, r)
called = true
}))
t.Cleanup(func() {
ts.Close()
})
u, err := url.Parse(ts.URL)
require.NoError(t, err)
w := httptest.NewRecorder()
r := httptest.NewRequest("POST", ts.URL, nil)
return u, &called, w, r
}
func TestProxyServeHTTP_AllowPostAndNotAuthenticated_WebAuthHeaderValueIsEmptyString(t *testing.T) {
u, called, w, r := setupForTest(t, func(w http.ResponseWriter, r *http.Request) {
// Note that if the header webAuthHeaderName hadn't been set then the value would be nil.
require.Equal(t, []string{""}, r.Header.Values(WebAuthHeaderName))
require.Equal(t, []string(nil), r.Header.Values("X-SOME-UNSET-HEADER"))
})
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return("")
proxy := newProxy(u, authMock, commonAllowed, true, false)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestProxyServeHTTP_UserIsLoggedIn_HeaderWithUserEmailIsIncludedInRequest(t *testing.T) {
u, called, w, r := setupForTest(t, assertValidEmailAndRole(t))
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return(viewerEmail)
proxy := newProxy(u, authMock, commonAllowed, false, false)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestProxyServeHTTP_UserIsLoggedInAndBelongsToTwoRoles_HeaderWithBothRolesIsIncludedInRequest(t *testing.T) {
u, called, w, r := setupForTest(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{viewerEmail}, r.Header.Values(WebAuthHeaderName))
receivedRoles := strings.Split(r.Header.Get(WebAuthRoleHeaderName), ",")
sort.Strings(receivedRoles)
expectedRoles := []string{
string(roles.Editor),
string(roles.Viewer),
}
require.Equal(t, expectedRoles, receivedRoles)
})
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return(viewerEmail)
allowedRoles := map[roles.Role]allowed.Allow{
roles.Viewer: allowed.NewAllowedFromList([]string{viewerEmail}),
roles.Editor: allowed.NewAllowedFromList([]string{viewerEmail}),
}
proxy := newProxy(u, authMock, allowedRoles, false, false)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestProxyServeHTTP_UserIsNotLoggedIn_HeaderWithUserEmailIsStrippedFromRequest(t *testing.T) {
u, called, w, r := setupForTest(t, func(w http.ResponseWriter, r *http.Request) {})
r.Header.Add(WebAuthHeaderName, viewerEmail) // Try to spoof the header.
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return("")
authMock.On("LoginURL", w, r).Return("http://example.org/login")
proxy := newProxy(u, authMock, commonAllowed, false, false)
proxy.ServeHTTP(w, r)
require.False(t, *called)
}
func TestProxyServeHTTP_UserIsLoggedInButNotAViewer_ReturnsStatusForbidden(t *testing.T) {
u, called, w, r := setupForTest(t, func(w http.ResponseWriter, r *http.Request) {})
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return(notAViewerEmail)
proxy := newProxy(u, authMock, commonAllowed, false, false)
proxy.ServeHTTP(w, r)
require.False(t, *called)
require.Equal(t, http.StatusForbidden, w.Result().StatusCode)
}
func TestProxyServeHTTP_UserIsLoggedIn_HeaderWithUserEmailIsIncludedInRequestAndSpoofedEmailIsRemoved(t *testing.T) {
u, called, w, r := setupForTest(t, assertValidEmailAndRole(t))
r.Header.Add(WebAuthHeaderName, "haxor@example.org") // Try to spoof the header.
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return(viewerEmail)
proxy := newProxy(u, authMock, commonAllowed, false, false)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestProxyServeHTTP_UserIsNotLoggedInAndPassiveFlagIsSet_RequestIsPassedAlongWithoutEmailHeader(t *testing.T) {
u, called, w, r := setupForTest(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, []string{""}, r.Header.Values(WebAuthHeaderName))
require.Equal(t, []string{""}, r.Header.Values(WebAuthRoleHeaderName))
})
r.Header.Add(WebAuthHeaderName, "haxor@example.org") // Try to spoof the header.
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return("")
proxy := newProxy(u, authMock, commonAllowed, false, true)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestProxyServeHTTP_UserIsLoggedInAndPassiveFlagIsSet_RequestIsPassedAlongWithEmailHeader(t *testing.T) {
u, called, w, r := setupForTest(t, assertValidEmailAndRole(t))
r.Header.Add(WebAuthHeaderName, "haxor@example.org") // Try to spoof the header.
authMock := mocks.NewAuth(t)
authMock.On("LoggedInAs", r).Return(viewerEmail)
proxy := newProxy(u, authMock, commonAllowed, false, true)
proxy.ServeHTTP(w, r)
require.True(t, *called)
}
func TestValidateFlags_OnlyRolesFlagsSpecified_ReturnsNoError(t *testing.T) {
app := &App{
roleFlags: []string{string(roles.Admin)},
criaGroup: "",
allowedFrom: "",
}
require.NoError(t, app.validateFlags())
}
func TestValidateFlags_BothLegacyAndRolesFlagsSpecified_ReturnsError(t *testing.T) {
app := &App{
roleFlags: []string{string(roles.Editor)},
criaGroup: "project-angle-committers",
allowedFrom: "",
}
require.Error(t, app.validateFlags())
}
func TestValidateFlags_BothLegacyFlagsSpecified_ReturnsError(t *testing.T) {
app := &App{
roleFlags: nil,
criaGroup: "project-angle-committers",
allowedFrom: "google.com",
}
require.Error(t, app.validateFlags())
}
func TestValidateFlags_NeitherLegacyFlagIsSpecified_ReturnsError(t *testing.T) {
app := &App{
roleFlags: nil,
criaGroup: "",
allowedFrom: "",
}
require.Error(t, app.validateFlags())
}
func TestValidateFlags_OnlyOneLegacyFlagIsSpecified_ReturnsNoError(t *testing.T) {
app := &App{
roleFlags: nil,
criaGroup: "project-angle-committers",
allowedFrom: "",
}
require.NoError(t, app.validateFlags())
app = &App{
roleFlags: nil,
criaGroup: "",
allowedFrom: "google.com",
}
require.NoError(t, app.validateFlags())
}
func TestAppRun_ContextIsCancelled_ReturnsNil(t *testing.T) {
// Construct minimal App.
target, err := url.Parse("http://my-service")
require.NoError(t, err)
app := &App{
target: target,
port: ":0",
promPort: ":0",
}
app.registerCleanup()
var w sync.WaitGroup
w.Add(1)
go func() {
err := app.Run(context.Background())
assert.NoError(t, err)
w.Done()
}()
// Ensure the server has been started.
for app.server == nil {
time.Sleep(time.Millisecond)
}
// Force a cleanup.
cleanup.Cleanup()
w.Wait()
// Test will fail by timeout if the app.Run() didn't return.
}
const testCriaGroupName = "mytestgroup"
const mockCriaResponse = `{
"group": {
"members": [
"user:test@example.org",
"user:*@chromium.org"
],
"globs": [
"user:*@gotham.org"
]
}
}`
func mockCriaClient(t *testing.T) *http.Client {
m := mockhttpclient.NewURLMock()
m.Mock(fmt.Sprintf(allowed.GROUP_URL_TEMPLATE, testCriaGroupName), mockhttpclient.MockGetDialogue([]byte(mockCriaResponse)))
return m.Client()
}
func TestAppPopulateLegacyAllowedRoles_LegacyCriaGroupFlag_Success(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.criaGroup = testCriaGroupName
err := a.populateLegacyAllowedRoles(m)
require.NoError(t, err)
require.True(t, a.allowedRoles[roles.Viewer].Member("fred@chromium.org"))
}
func TestAppPopulateLegacyAllowedRoles_LegacyCriaGroupFlagHasInvalidGroup_ReturnsErro(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.criaGroup = "unknown-group"
err := a.populateLegacyAllowedRoles(m)
require.Error(t, err)
}
func TestAppPopulateAllowedRoles_MultipleGroups_Success(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"editor=cria_group:" + testCriaGroupName,
"admin=google.com",
}
err := a.populateAllowedRoles(m)
require.NoError(t, err)
require.True(t, a.allowedRoles[roles.Editor].Member("fred@chromium.org"))
require.False(t, a.allowedRoles[roles.Admin].Member("fred@chromium.org"))
require.True(t, a.allowedRoles[roles.Admin].Member("barney@google.com"))
}
func TestAppPopulateAllowedRoles_MultipleGroupsSameRoles_RoleContainsUnionOfAllows(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"editor=cria_group:" + testCriaGroupName,
"editor=google.com",
}
err := a.populateAllowedRoles(m)
require.NoError(t, err)
require.True(t, a.allowedRoles[roles.Editor].Member("fred@chromium.org"))
require.True(t, a.allowedRoles[roles.Editor].Member("barney@google.com"))
}
func TestAppPopulateAllowedRoles_InvalidCriaGroup_ReturnsError(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"editor=cria_group:this-is-not-a-valid-group",
"admin=google.com",
}
err := a.populateAllowedRoles(m)
require.Contains(t, err.Error(), "Failed parsing")
}
func TestAppPopulateAllowedRoles_UnknownRole_ReturnsError(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"not-a-known-role=cria_group:" + testCriaGroupName,
}
err := a.populateAllowedRoles(m)
require.Contains(t, err.Error(), "Invalid Role")
}
func TestAppPopulateAllowedRoles_BadFlagFormat_ReturnsError(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"too=many=equals",
}
err := a.populateAllowedRoles(m)
require.Contains(t, err.Error(), "Invalid format")
}
func TestAppPopulateAllowedRoles_CommaSeparatedRoles_RoleContainsUnionOfAllows(t *testing.T) {
m := mockCriaClient(t)
a := newEmptyApp()
a.roleFlags = []string{
"viewer=google.com chromium.org",
}
err := a.populateAllowedRoles(m)
require.NoError(t, err)
require.True(t, a.allowedRoles[roles.Viewer].Member("fred@chromium.org"))
require.True(t, a.allowedRoles[roles.Viewer].Member("barney@google.com"))
}
func TestAppPopulateAllowedRoles_TestMultiFlagParsing(t *testing.T) {
a := newEmptyApp()
err := a.Flagset().Parse([]string{"--role=viewer=google.com chromium.org", "--role=editor=google.com chromium.org"})
require.NoError(t, err)
expected := []string{
"viewer=google.com chromium.org",
"editor=google.com chromium.org",
}
require.Equal(t, expected, a.roleFlags)
}
func TestToAuthType_ValidType_ReturnsTypeUnchanged(t *testing.T) {
for _, typ := range AllValidAuthTypes {
require.Equal(t, typ, ToAuthType(string(typ)))
}
}
func TestToAuthType_UnknownTypes_ReturnsInvalid(t *testing.T) {
require.Equal(t, Invalid, ToAuthType("this is not a valid auth type"))
}
func TestParseTargetPort_OnlyPortIsSupplied_LocalhostUsedAsDomain(t *testing.T) {
got, err := parseTargetPort(":8000")
require.NoError(t, err)
require.Equal(t, "http://localhost:8000", got.String())
}
func TestParseTargetPort_FullURLIsSupplied_LocalhostInNotAddedToDomain(t *testing.T) {
got, err := parseTargetPort("http://foo:8000")
require.NoError(t, err)
require.Equal(t, "http://foo:8000", got.String())
}