mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 02:44:59 +00:00
* removes unused filesize function * removes ioutil usage * updates ioutil.ReadAll to io.ReadAll * updates ioutil.TempFile to os.CreateTemp * updates ioutil.TempDir to os.MkdirTemp * updates ioutil.ReadAll to os.ReadAll * update ioutil.WriteFile to os.WriteFile * updates ioutil.Discard to io.Discard * updates ioutil.ReadDir to os.ReadDir where applicable * removes unused code in idk * creates type to use for context value keys * replaces assert.Nil with assert.NoError for error checks
1060 lines
30 KiB
Go
1060 lines
30 KiB
Go
// Copyright 2022 Molecula Corp. (DBA FeatureBase).
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
package pilosa
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/authn"
|
|
"github.com/golang-jwt/jwt"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/authz"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/featurebasedb/featurebase/v3/pql"
|
|
)
|
|
|
|
// Test custom UnmarshalJSON for postIndexRequest object
|
|
func TestPostIndexRequestUnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
json string
|
|
expected postIndexRequest
|
|
err string
|
|
}{
|
|
{json: `{"options": {}}`, expected: postIndexRequest{Options: IndexOptions{TrackExistence: true}}},
|
|
{json: `{"options": {"trackExistence": false}}`, expected: postIndexRequest{Options: IndexOptions{TrackExistence: false}}},
|
|
{json: `{"options": {"keys": true}}`, expected: postIndexRequest{Options: IndexOptions{Keys: true, TrackExistence: true}}},
|
|
{json: `{"options": 4}`, err: "options is not map[string]interface{}"},
|
|
{json: `{"option": {}}`, err: "unknown key: option:map[]"},
|
|
{json: `{"options": {"badKey": "test"}}`, err: "unknown key: badKey:test"},
|
|
}
|
|
for _, test := range tests {
|
|
actual := &postIndexRequest{}
|
|
err := json.Unmarshal([]byte(test.json), actual)
|
|
|
|
if err != nil {
|
|
if test.err == "" || test.err != err.Error() {
|
|
t.Errorf("expected error: %v, but got result: %v", test.err, err)
|
|
}
|
|
} else {
|
|
if test.err != "" {
|
|
t.Errorf("expected error: %v, but got no error", test.err)
|
|
}
|
|
}
|
|
|
|
if test.err == "" {
|
|
if !reflect.DeepEqual(*actual, test.expected) {
|
|
t.Errorf("expected: %v, but got: %v for JSON: %s", test.expected, *actual, test.json)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test custom UnmarshalJSON for postFieldRequest object
|
|
func TestPostFieldRequestUnmarshalJSON(t *testing.T) {
|
|
foo := "foo"
|
|
tests := []struct {
|
|
json string
|
|
expected postFieldRequest
|
|
err string
|
|
}{
|
|
{json: `{"options": {}}`, expected: postFieldRequest{}},
|
|
{json: `{"options": 4}`, err: "json: cannot unmarshal number"},
|
|
{json: `{"option": {}}`, err: `json: unknown field "option"`},
|
|
{json: `{"options": {"badKey": "test"}}`, err: `json: unknown field "badKey"`},
|
|
{json: `{"options": {"inverseEnabled": true}}`, err: `json: unknown field "inverseEnabled"`},
|
|
{json: `{"options": {"cacheType": "foo"}}`, expected: postFieldRequest{Options: fieldOptions{CacheType: &foo}}},
|
|
{json: `{"options": {"inverse": true, "cacheType": "foo"}}`, err: `json: unknown field "inverse"`},
|
|
}
|
|
for i, test := range tests {
|
|
actual := &postFieldRequest{}
|
|
dec := json.NewDecoder(bytes.NewReader([]byte(test.json)))
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(actual)
|
|
if err != nil {
|
|
if test.err == "" || !strings.HasPrefix(err.Error(), test.err) {
|
|
t.Errorf("test %d: expected error: %v, but got result: %v", i, test.err, err)
|
|
}
|
|
}
|
|
|
|
if test.err == "" {
|
|
if !reflect.DeepEqual(*actual, test.expected) {
|
|
t.Errorf("test %d: expected: %v, but got: %v", i, test.expected, *actual)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stringPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func decimalPtr(d pql.Decimal) *pql.Decimal {
|
|
return &d
|
|
}
|
|
|
|
// Test fieldOption validation.
|
|
func TestFieldOptionValidation(t *testing.T) {
|
|
timeQuantum := TimeQuantum("YMD")
|
|
defaultCacheSize := uint32(DefaultCacheSize)
|
|
tests := []struct {
|
|
json string
|
|
expected postFieldRequest
|
|
err string
|
|
}{
|
|
// FieldType: Set
|
|
{json: `{"options": {}}`, expected: postFieldRequest{Options: fieldOptions{
|
|
Type: FieldTypeSet,
|
|
CacheType: stringPtr(DefaultCacheType),
|
|
CacheSize: &defaultCacheSize,
|
|
}}},
|
|
{json: `{"options": {"type": "set"}}`, expected: postFieldRequest{Options: fieldOptions{
|
|
Type: FieldTypeSet,
|
|
CacheType: stringPtr(DefaultCacheType),
|
|
CacheSize: &defaultCacheSize,
|
|
}}},
|
|
{json: `{"options": {"type": "set", "cacheType": "lru"}}`, expected: postFieldRequest{Options: fieldOptions{
|
|
Type: FieldTypeSet,
|
|
CacheType: stringPtr("lru"),
|
|
CacheSize: &defaultCacheSize,
|
|
}}},
|
|
{json: `{"options": {"type": "set", "min": 0}}`, err: "min does not apply to field type set"},
|
|
{json: `{"options": {"type": "set", "max": 100}}`, err: "max does not apply to field type set"},
|
|
{json: `{"options": {"type": "set", "timeQuantum": "YMD"}}`, err: "timeQuantum does not apply to field type set"},
|
|
{json: `{"options": {"type": "set", "ttl": "1h"}}`, err: "ttl does not apply to field type set"},
|
|
|
|
// FieldType: Int
|
|
{json: `{"options": {"type": "int"}}`, err: "min is required for field type int"},
|
|
{json: `{"options": {"type": "int", "min": 0}}`, err: "max is required for field type int"},
|
|
{json: `{"options": {"type": "int", "min": 0, "max": 1001}}`, expected: postFieldRequest{Options: fieldOptions{
|
|
Type: FieldTypeInt,
|
|
Min: decimalPtr(pql.NewDecimal(0, 0)),
|
|
Max: decimalPtr(pql.NewDecimal(1001, 0)),
|
|
}}},
|
|
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheType": "ranked"}}`, err: "cacheType does not apply to field type int"},
|
|
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "cacheSize": 1000}}`, err: "cacheSize does not apply to field type int"},
|
|
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "timeQuantum": "YMD"}}`, err: "timeQuantum does not apply to field type int"},
|
|
{json: `{"options": {"type": "int", "min": 0, "max": 1000, "ttl": "1h"}}`, err: "ttl does not apply to field type int"},
|
|
|
|
// FieldType: Time
|
|
{json: `{"options": {"type": "time"}}`, err: "timeQuantum is required for field type time"},
|
|
{json: `{"options": {"type": "time", "timeQuantum": "YMD"}}`, expected: postFieldRequest{Options: fieldOptions{
|
|
Type: FieldTypeTime,
|
|
TimeQuantum: &timeQuantum,
|
|
}}},
|
|
{json: `{"options": {"type": "time", "timeQuantum": "YMD", "min": 0}}`, err: "min does not apply to field type time"},
|
|
{json: `{"options": {"type": "time", "timeQuantum": "YMD", "max": 1000}}`, err: "max does not apply to field type time"},
|
|
{json: `{"options": {"type": "time", "timeQuantum": "YMD", "cacheType": "ranked"}}`, err: "cacheType does not apply to field type time"},
|
|
{json: `{"options": {"type": "time", "timeQuantum": "YMD", "cacheSize": 1000}}`, err: "cacheSize does not apply to field type time"},
|
|
}
|
|
for i, test := range tests {
|
|
actual := &postFieldRequest{}
|
|
dec := json.NewDecoder(bytes.NewReader([]byte(test.json)))
|
|
dec.DisallowUnknownFields()
|
|
err := dec.Decode(actual)
|
|
if err != nil {
|
|
t.Errorf("test %d: %v", i, err)
|
|
}
|
|
|
|
// Validate field options.
|
|
if err := actual.Options.validate(); err != nil {
|
|
if test.err == "" || test.err != err.Error() {
|
|
t.Errorf("test %d: expected error: %v, but got result: %v", i, test.err, err)
|
|
}
|
|
}
|
|
|
|
if test.err == "" {
|
|
if !reflect.DeepEqual(*actual, test.expected) {
|
|
t.Errorf("test %d: expected: %v, but got: %v", i, test.expected, *actual)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func readResponse(w *httptest.ResponseRecorder) ([]byte, error) {
|
|
res := w.Result()
|
|
defer res.Body.Close()
|
|
return io.ReadAll(res.Body)
|
|
}
|
|
|
|
// common variables used for testing auth
|
|
var (
|
|
ClientID = "e9088663-eb08-41d7-8f65-efb5f54bbb71"
|
|
ClientSecret = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
|
AuthorizeURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/authorize"
|
|
TokenURL = "https://login.microsoftonline.com/4a137d66-d161-4ae4-b1e6-07e9920874b8/oauth2/v2.0/token"
|
|
GroupEndpointURL = "https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true"
|
|
LogoutURL = "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
|
|
Scopes = []string{"https://graph.microsoft.com/.default", "offline_access"}
|
|
Key = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
|
ConfiguredIPs = []string{}
|
|
)
|
|
|
|
func TestAuthentication(t *testing.T) {
|
|
type evaluate func(w *httptest.ResponseRecorder, data []byte)
|
|
type endpoint func(w http.ResponseWriter, r *http.Request)
|
|
|
|
type Group struct {
|
|
GroupID string `json:"id"`
|
|
GroupName string `json:"displayName"`
|
|
}
|
|
|
|
// Groups holds a slice of Group for marshalling from JSON
|
|
type Groups struct {
|
|
NextLink string `json:"@odata.nextLink"`
|
|
Groups []Group `json:"value"`
|
|
}
|
|
|
|
groupSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, err := json.Marshal(
|
|
Groups{
|
|
Groups: []Group{
|
|
{
|
|
GroupID: "what are you?",
|
|
GroupName: "i am a carbon-based bipedal life form descended from an ape",
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error marshalling groups response: %v", err)
|
|
}
|
|
fmt.Fprintf(w, "%s", body)
|
|
}))
|
|
defer groupSrv.Close()
|
|
|
|
GroupEndpointURL = groupSrv.URL
|
|
secretKey, _ := hex.DecodeString(Key)
|
|
|
|
a := NewTestAuth(t)
|
|
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: a,
|
|
}
|
|
|
|
hOff := Handler{}
|
|
|
|
// make a valid token
|
|
tkn := jwt.New(jwt.SigningMethodHS256)
|
|
claims := tkn.Claims.(jwt.MapClaims)
|
|
claims["oid"] = "42"
|
|
claims["name"] = "todd"
|
|
validToken, err := tkn.SignedString([]byte(secretKey))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
validToken = "Bearer " + validToken
|
|
|
|
token := oauth2.Token{
|
|
TokenType: "Bearer",
|
|
AccessToken: "asdf",
|
|
RefreshToken: "abcdef",
|
|
Expiry: time.Now().Add(time.Hour),
|
|
}
|
|
|
|
// make an expired token
|
|
claims["exp"] = "1"
|
|
expiredToken, err := tkn.SignedString([]byte(secretKey))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expiredToken = "Bearer " + expiredToken
|
|
|
|
validCookie := &http.Cookie{
|
|
Name: authn.AccessCookieName,
|
|
Value: token.AccessToken,
|
|
Path: "/",
|
|
Secure: true,
|
|
HttpOnly: true,
|
|
Expires: token.Expiry,
|
|
}
|
|
|
|
permissions1 := `"user-groups":
|
|
"dca35310-ecda-4f23-86cd-876aee559900":
|
|
"test": "write"
|
|
admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"`
|
|
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
kind string
|
|
method string
|
|
yamlData string
|
|
token string
|
|
cookie *http.Cookie
|
|
handler endpoint
|
|
fn evaluate
|
|
}{
|
|
{
|
|
name: "Login",
|
|
path: "/login",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: h.handleLogin,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
t.Errorf("incorrect redirect url: expected: %s, got: %s", AuthorizeURL, string(data))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Logout",
|
|
path: "/logout",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: h.handleLogout,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if w.Result().Cookies()[0].Value != "" {
|
|
t.Errorf("expected cookie to be cleared, got: %+v", w.Result().Cookies()[0].Value)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Authenticate-ValidToken",
|
|
path: "/auth",
|
|
kind: "bearer",
|
|
token: validToken,
|
|
handler: h.handleCheckAuthentication,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if w.Result().StatusCode != 200 {
|
|
body, _ := readResponse(w)
|
|
t.Errorf("expected http code 200, got: %+v with body: %+v", w.Result().StatusCode, body)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Authenticate-NoToken",
|
|
path: "/auth",
|
|
kind: "type1",
|
|
handler: h.handleCheckAuthentication,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
// not token at all == status forbidden
|
|
if w.Result().StatusCode != 401 {
|
|
t.Errorf("expected http code 401, got: %+v", w.Result().StatusCode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Authenticate-InvalidToken",
|
|
path: "/auth",
|
|
kind: "type1",
|
|
token: "this isn't a real token",
|
|
handler: h.handleCheckAuthentication,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
// no valid token in header == Unauthorized
|
|
if w.Result().StatusCode != http.StatusUnauthorized {
|
|
t.Errorf("expected http code 401, got: %+v", w.Result().StatusCode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Authenticate-ExpiredToken",
|
|
path: "/auth",
|
|
kind: "type1",
|
|
token: expiredToken,
|
|
handler: h.handleCheckAuthentication,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
// expired token == unauthorized
|
|
if w.Result().StatusCode != 401 {
|
|
t.Errorf("expected http code 403, got: %+v", w.Result().StatusCode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "UserInfo",
|
|
path: "/userinfo",
|
|
kind: "bearer",
|
|
token: validToken,
|
|
handler: h.handleUserInfo,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
uinfo := authn.UserInfo{}
|
|
err = json.Unmarshal(data, &uinfo)
|
|
if err != nil {
|
|
t.Errorf("unmarshalling userinfo")
|
|
}
|
|
if uinfo.UserID != "42" && uinfo.UserName != "todd" {
|
|
t.Errorf("expected http code 400, got: %+v", uinfo)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "UserInfo-NoCookie",
|
|
path: "/userinfo",
|
|
kind: "bearer",
|
|
token: "",
|
|
handler: h.handleUserInfo,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if got := w.Result().StatusCode; got != http.StatusForbidden {
|
|
t.Errorf("expected 403, got %v", got)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Redirect-NoAuthCode",
|
|
path: "/redirect",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: h.handleRedirect,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 400 {
|
|
t.Errorf("expected http code 400, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Redirect-SomeAuthCode",
|
|
path: "/redirect",
|
|
kind: "type2",
|
|
cookie: validCookie,
|
|
handler: h.handleRedirect,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 400 {
|
|
t.Errorf("expected http code 400, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Login-AuthOff",
|
|
path: "/login",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: hOff.handleLogin,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 204 {
|
|
t.Errorf("expected http code 204, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Logout-AuthOff",
|
|
path: "/logout",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: hOff.handleLogout,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 204 {
|
|
t.Errorf("expected http code 204, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "UserInfo-AuthOff",
|
|
path: "/userinfo",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: hOff.handleUserInfo,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 204 {
|
|
t.Errorf("expected http code 204, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Authenticate-AuthOff",
|
|
path: "/auth",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: hOff.handleCheckAuthentication,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 204 {
|
|
t.Errorf("expected http code 204, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Redirect-AuthOff",
|
|
path: "/redirect",
|
|
kind: "type1",
|
|
cookie: validCookie,
|
|
handler: hOff.handleRedirect,
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if strings.Index(string(data), AuthorizeURL) != 9 {
|
|
if w.Result().StatusCode != 204 {
|
|
t.Errorf("expected http code 204, got: %+v", w.Result().StatusCode)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "MW-AuthOff",
|
|
path: "/index/{index}/query",
|
|
kind: "middleware",
|
|
cookie: validCookie,
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
f := hOff.chkAuthZ(hOff.handlePostQuery, authz.Admin)
|
|
f(w, r)
|
|
},
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if w.Result().StatusCode != 400 {
|
|
t.Errorf("expected http code 400, got: %+v", w.Result().StatusCode)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "MW-CreateIndexInsufficientPerms",
|
|
path: "/index/abcd",
|
|
kind: "bearer",
|
|
method: http.MethodPost,
|
|
token: validToken,
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
h := h
|
|
var p authz.GroupPermissions
|
|
if err := p.ReadPermissionsFile(strings.NewReader(permissions1)); err != nil {
|
|
t.Errorf("Error: %s", err)
|
|
}
|
|
h.permissions = &p
|
|
|
|
f := h.chkAuthZ(h.handlePostIndex, authz.Admin)
|
|
f(w, r)
|
|
},
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if got, want := w.Result().StatusCode, http.StatusForbidden; got != want {
|
|
t.Errorf("expected %v, got %v", want, got)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
// this tests that there are no permissions read in even though
|
|
// auth is turned on, so we get a 500
|
|
name: "MW-NoPermissions",
|
|
path: "/index/{index}/query",
|
|
kind: "bearer",
|
|
token: validToken,
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
h := h
|
|
f := h.chkAuthZ(h.handlePostQuery, authz.Write)
|
|
f(w, r)
|
|
},
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if got, want := w.Result().StatusCode, http.StatusInternalServerError; got != want {
|
|
t.Errorf("expected %v, got %v", want, got)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "MW-NoQuery",
|
|
path: "/index/{index}/query",
|
|
kind: "bearer",
|
|
token: validToken,
|
|
handler: func(w http.ResponseWriter, r *http.Request) {
|
|
h := h
|
|
var p authz.GroupPermissions
|
|
if err := p.ReadPermissionsFile(strings.NewReader(permissions1)); err != nil {
|
|
t.Errorf("Error: %s", err)
|
|
}
|
|
h.permissions = &p
|
|
f := h.chkAuthZ(h.handlePostQuery, authz.Write)
|
|
f(w, r)
|
|
},
|
|
fn: func(w *httptest.ResponseRecorder, data []byte) {
|
|
if got, want := w.Result().StatusCode, http.StatusBadRequest; got != want {
|
|
t.Errorf("expected %v, got: %+v", want, got)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
switch test.kind {
|
|
case "type1", "middleware":
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, test.path, nil)
|
|
w := httptest.NewRecorder()
|
|
if test.cookie != nil {
|
|
r.AddCookie(test.cookie)
|
|
}
|
|
test.handler(w, r)
|
|
data, err := readResponse(w)
|
|
if err != nil {
|
|
t.Errorf("expected no errors reading response, got: %+v", err)
|
|
}
|
|
test.fn(w, data)
|
|
})
|
|
case "type2":
|
|
t.Run(test.name, func(t *testing.T) {
|
|
r := httptest.NewRequest(http.MethodGet, test.path, nil)
|
|
w := httptest.NewRecorder()
|
|
r.Form = url.Values{}
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
r.Form.Add("code", "junk")
|
|
|
|
test.handler(w, r)
|
|
data, err := readResponse(w)
|
|
if err != nil {
|
|
t.Errorf("expected no errors reading response, got: %+v", err)
|
|
}
|
|
|
|
test.fn(w, data)
|
|
|
|
})
|
|
case "bearer":
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.method == "" {
|
|
test.method = http.MethodGet
|
|
}
|
|
r := httptest.NewRequest(test.method, test.path, nil)
|
|
w := httptest.NewRecorder()
|
|
if test.token != "" {
|
|
r.Header.Add("Authorization", test.token)
|
|
}
|
|
test.handler(w, r)
|
|
data, err := readResponse(w)
|
|
if err != nil {
|
|
t.Errorf("expected no errors reading response, got: %+v", err)
|
|
}
|
|
test.fn(w, data)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func TestChkAuthN(t *testing.T) {
|
|
a := NewTestAuth(t)
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: a,
|
|
}
|
|
|
|
// make a valid token
|
|
tkn := jwt.New(jwt.SigningMethodHS256)
|
|
claims := tkn.Claims.(jwt.MapClaims)
|
|
claims["oid"] = "42"
|
|
claims["name"] = "A. Token"
|
|
validToken, err := tkn.SignedString(a.SecretKey())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
validToken = "Bearer " + validToken
|
|
|
|
// make an invalid token
|
|
invalidToken := "Bearer " + "thisis.a.bad.token"
|
|
|
|
// make an expired token
|
|
claims["exp"] = "1"
|
|
expiredToken, err := tkn.SignedString(a.SecretKey())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expiredToken = "Bearer " + expiredToken
|
|
|
|
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte("good"))
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
endpoint string
|
|
token string
|
|
handler http.HandlerFunc
|
|
err string
|
|
}{
|
|
{
|
|
name: "ValidToken-ButNotForMicrosoft",
|
|
token: validToken,
|
|
handler: h.chkAuthN(testingHandler),
|
|
err: "authenticating: getting groups: getting group membership info",
|
|
},
|
|
{
|
|
name: "Invalid",
|
|
token: invalidToken,
|
|
handler: h.chkAuthN(testingHandler),
|
|
err: "authenticating: parsing auth token",
|
|
},
|
|
{
|
|
name: "Expired",
|
|
token: expiredToken,
|
|
handler: h.chkAuthN(testingHandler),
|
|
err: "authenticating: token is expired",
|
|
},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/whatever", nil)
|
|
r.Header.Add("Authorization", test.token)
|
|
test.handler(w, r)
|
|
resp := w.Result()
|
|
body, err := io.ReadAll(resp.Body)
|
|
defer resp.Body.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(string(body), test.err) {
|
|
t.Fatalf("expected error %s, got: %s", test.err, string(body))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestChkInternal(t *testing.T) {
|
|
a := NewTestAuth(t)
|
|
authKey := "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: a,
|
|
}
|
|
|
|
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("good"))
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
statusCode int
|
|
handler http.HandlerFunc
|
|
key string
|
|
}{
|
|
{
|
|
name: "happyPath",
|
|
statusCode: http.StatusOK,
|
|
handler: h.chkInternal(testingHandler),
|
|
key: authKey,
|
|
},
|
|
{
|
|
name: "unhappyPath-empty",
|
|
statusCode: http.StatusUnauthorized,
|
|
handler: h.chkInternal(testingHandler),
|
|
key: "",
|
|
},
|
|
{
|
|
name: "unhappyPath-wrong",
|
|
statusCode: http.StatusUnauthorized,
|
|
handler: h.chkInternal(testingHandler),
|
|
key: "BEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEFBEABBEEF",
|
|
},
|
|
}
|
|
for _, test := range cases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/whatever", nil)
|
|
if test.key != "" {
|
|
r.Header.Add("X-Feature-Key", test.key)
|
|
}
|
|
test.handler(w, r)
|
|
resp := w.Result()
|
|
if resp.StatusCode != test.statusCode {
|
|
t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func NewTestAuth(t *testing.T) *authn.Auth {
|
|
t.Helper()
|
|
a, err := authn.NewAuth(
|
|
logger.NewStandardLogger(os.Stdout),
|
|
"http://localhost:10101/",
|
|
Scopes,
|
|
AuthorizeURL,
|
|
TokenURL,
|
|
GroupEndpointURL,
|
|
LogoutURL,
|
|
ClientID,
|
|
ClientSecret,
|
|
Key,
|
|
ConfiguredIPs,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("building auth object%s", err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func TestHandleGetMemUsage(t *testing.T) {
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/whatever", nil)
|
|
|
|
h.handleGetMemUsage(w, r)
|
|
|
|
resp := w.Result()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetDiskUsage(t *testing.T) {
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
api: &API{
|
|
server: &Server{
|
|
dataDir: t.TempDir(),
|
|
},
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/whatever", nil)
|
|
|
|
h.handleGetDiskUsage(w, r)
|
|
|
|
resp := w.Result()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHandleOAuthConfig(t *testing.T) {
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: NewTestAuth(t),
|
|
api: &API{
|
|
server: &Server{
|
|
dataDir: t.TempDir(),
|
|
},
|
|
},
|
|
}
|
|
w := httptest.NewRecorder()
|
|
r := httptest.NewRequest("GET", "/whatever", nil)
|
|
|
|
h.handleOAuthConfig(w, r)
|
|
|
|
resp := w.Result()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected %v, got %v", http.StatusOK, resp.StatusCode)
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
var rsp oauth2.Config
|
|
if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil {
|
|
t.Fatalf("unexpected error decoding body: %v", err)
|
|
}
|
|
if exp := h.auth.CleanOAuthConfig(); !reflect.DeepEqual(exp, rsp) {
|
|
t.Fatalf("expected %v, got %v", exp, rsp)
|
|
}
|
|
}
|
|
|
|
func TestAuthzAllowedIPs(t *testing.T) {
|
|
tests := []struct {
|
|
configuredIPs []string
|
|
clientIP string
|
|
statusCode int
|
|
permission authz.Permission
|
|
}{
|
|
// client IP is in configured IP list
|
|
{
|
|
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
|
clientIP: "10.0.0.0",
|
|
statusCode: http.StatusOK,
|
|
permission: authz.Admin,
|
|
},
|
|
// client IP is in configured IP list, testing with CIDR address
|
|
{
|
|
configuredIPs: []string{"10.0.0.0/30"},
|
|
clientIP: "10.0.0.1",
|
|
statusCode: http.StatusOK,
|
|
permission: authz.Write,
|
|
},
|
|
// client IP has multiple IPs in X-Forwarded-For header
|
|
// originating IP is in configured IP list
|
|
{
|
|
configuredIPs: []string{"10.0.0.0", "10.0.0.1/32", "10.0.0.2"},
|
|
clientIP: "10.0.0.2,10.0.0.255",
|
|
statusCode: http.StatusOK,
|
|
permission: authz.Read,
|
|
},
|
|
// client IP has multiple IPs in X-Forwarded-For header
|
|
// originating IP is not in configured IP list
|
|
{
|
|
configuredIPs: []string{"10.0.0.0/30"},
|
|
clientIP: "10.0.0.255,10.0.0.2",
|
|
statusCode: http.StatusForbidden,
|
|
permission: authz.Read,
|
|
},
|
|
// client IP is not in configured IP list
|
|
{
|
|
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
|
clientIP: "10.0.0.3",
|
|
statusCode: http.StatusForbidden,
|
|
permission: authz.Write,
|
|
},
|
|
// client IP is not in configured IP list
|
|
// X-Forwarded-For header is an empty string
|
|
{
|
|
configuredIPs: []string{"10.0.0.0", "10.0.0.1", "10.0.2.0/32"},
|
|
clientIP: "",
|
|
statusCode: http.StatusForbidden,
|
|
permission: authz.Write,
|
|
},
|
|
}
|
|
|
|
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("good"))
|
|
}
|
|
|
|
permissions1 := `"user-groups":
|
|
"dca35310-ecda-4f23-86cd-876aee559900":
|
|
"test": "write"
|
|
admin: "ac97c9e2-346b-42a2-b6da-18bcb61a32fe"`
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("ChkAuthz-%d", i), func(t *testing.T) {
|
|
ConfiguredIPs = test.configuredIPs
|
|
a := NewTestAuth(t)
|
|
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: a,
|
|
}
|
|
|
|
var p authz.GroupPermissions
|
|
if err := p.ReadPermissionsFile(strings.NewReader(permissions1)); err != nil {
|
|
t.Errorf("Error: %s", err)
|
|
}
|
|
h.permissions = &p
|
|
|
|
r := httptest.NewRequest("GET", "/index/authz-abcd", nil)
|
|
r.Header.Set(ForwardedIPHeader, test.clientIP)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := h.chkAuthZ(testingHandler, authz.Read)
|
|
handler(w, r)
|
|
resp := w.Result()
|
|
if resp.StatusCode != test.statusCode {
|
|
t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthnAllowedIPs(t *testing.T) {
|
|
IPList := []string{"10.0.0.0", "10.0.0.1", "10.0.0.2"}
|
|
ValidForwardedIP := "10.0.0.0, 10.0.0.3, 10.0.0.4"
|
|
InvalidForwardedIP := "10.0.0.3, 10.0.0.4"
|
|
|
|
tests := []struct {
|
|
configuredIPs []string
|
|
clientIP string
|
|
statusCode int
|
|
secretKey string
|
|
}{
|
|
// test client IP was in configured IP list - happy path
|
|
{
|
|
configuredIPs: IPList,
|
|
clientIP: IPList[0],
|
|
statusCode: http.StatusOK,
|
|
},
|
|
// test empty configured IP list
|
|
{
|
|
configuredIPs: []string{""},
|
|
clientIP: IPList[0],
|
|
statusCode: http.StatusUnauthorized,
|
|
},
|
|
// test client IP is not in configured IP list
|
|
{
|
|
configuredIPs: IPList,
|
|
clientIP: "10.0.0.4",
|
|
statusCode: http.StatusUnauthorized,
|
|
},
|
|
// test multiple client IPs in X-forwarded-IP
|
|
// originating IP is in configured IP list
|
|
{
|
|
configuredIPs: IPList,
|
|
clientIP: ValidForwardedIP,
|
|
statusCode: http.StatusOK,
|
|
},
|
|
// test multiple client IPs in X-forwarded-IP
|
|
// originating IP is not in configured IP list
|
|
{
|
|
configuredIPs: IPList,
|
|
clientIP: InvalidForwardedIP,
|
|
statusCode: http.StatusUnauthorized,
|
|
},
|
|
}
|
|
|
|
testingHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte("good"))
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("ChkAuthn-%d", i), func(t *testing.T) {
|
|
ConfiguredIPs = test.configuredIPs
|
|
a := NewTestAuth(t)
|
|
|
|
h := Handler{
|
|
logger: logger.NewStandardLogger(os.Stdout),
|
|
queryLogger: logger.NewStandardLogger(os.Stdout),
|
|
auth: a,
|
|
}
|
|
|
|
r := httptest.NewRequest("GET", "/index/authn-abcd", nil)
|
|
r.Header.Set(ForwardedIPHeader, test.clientIP)
|
|
r = r.WithContext(context.Background())
|
|
w := httptest.NewRecorder()
|
|
|
|
handler := h.chkAuthN(testingHandler)
|
|
handler(w, r)
|
|
resp := w.Result()
|
|
if resp.StatusCode != test.statusCode {
|
|
t.Fatalf("expected %v, got %v", test.statusCode, resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_scrubPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
pathParts []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "happyPath",
|
|
pathParts: []string{"", "index", "iname", "field", "fname"},
|
|
want: "/index/{index}/field/{field}",
|
|
},
|
|
{
|
|
name: "kindaHappyPath",
|
|
pathParts: []string{"", "index", "iname", "field", "fname"},
|
|
want: "/index/{index}/field/{field}",
|
|
},
|
|
{
|
|
name: "sadPath",
|
|
pathParts: []string{"", "notindex", "iname", "field", "fname", "field2", "fname2"},
|
|
want: "/notindex/iname/field/fname/field2/fname2",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := scrubPath(tt.pathParts); got != tt.want {
|
|
t.Errorf("scrubPath() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|