mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-16 04:48:04 +00:00
79fb4cc1cc
# Which Problems Are Solved System administrators can block hosts and IPs for HTTP calls in actions. Using DNS, blocked IPs could be bypassed. # How the Problems Are Solved - Hosts are resolved (DNS lookup) to check whether their corresponding IP is blocked. # Additional Changes - Added complete lookup ip address range and "unspecified" address to the default `DenyList`
468 lines
9.6 KiB
Go
468 lines
9.6 KiB
Go
package actions
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/dop251/goja"
|
|
|
|
"github.com/zitadel/zitadel/internal/logstore"
|
|
"github.com/zitadel/zitadel/internal/logstore/record"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
func Test_isHostBlocked(t *testing.T) {
|
|
SetLogstoreService(logstore.New[*record.ExecutionLog](nil, nil))
|
|
var denyList = []AddressChecker{
|
|
mustNewHostChecker(t, "192.168.5.0/24"),
|
|
mustNewHostChecker(t, "127.0.0.1"),
|
|
mustNewHostChecker(t, "test.com"),
|
|
}
|
|
type fields struct {
|
|
lookup func(host string) ([]net.IP, error)
|
|
}
|
|
type args struct {
|
|
address *url.URL
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
want bool
|
|
}{
|
|
{
|
|
name: "in range",
|
|
args: args{
|
|
address: mustNewURL(t, "https://192.168.5.4/hodor"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "exact ip",
|
|
args: args{
|
|
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "address match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test.com:42/hodor"),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "address not match",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("194.264.52.4")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "looked up ip matches",
|
|
fields: fields{
|
|
lookup: func(host string) ([]net.IP, error) {
|
|
return []net.IP{net.ParseIP("127.0.0.1")}, nil
|
|
},
|
|
},
|
|
args: args{
|
|
address: mustNewURL(t, "https://test2.com/hodor"),
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
trans := &transport{
|
|
lookup: tt.fields.lookup,
|
|
}
|
|
if got := trans.isHostBlocked(denyList, tt.args.address); got != tt.want {
|
|
t.Errorf("isHostBlocked() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
|
|
t.Helper()
|
|
checker, err := NewHostChecker(ip)
|
|
if err != nil {
|
|
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
|
|
t.FailNow()
|
|
}
|
|
return checker
|
|
}
|
|
|
|
func mustNewURL(t *testing.T, raw string) *url.URL {
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
t.Errorf("unable to parse address of %q because: %v", raw, err)
|
|
t.FailNow()
|
|
}
|
|
return u
|
|
}
|
|
|
|
func TestHTTP_fetchConfigFromArg(t *testing.T) {
|
|
runtime := goja.New()
|
|
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
|
|
|
type args struct {
|
|
arg *goja.Object
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantConfig fetchConfig
|
|
wantErr func(error) bool
|
|
}{
|
|
{
|
|
name: "no fetch option provided",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
struct{}{},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "header set as string",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Headers map[string]string
|
|
}{
|
|
Headers: map[string]string{
|
|
"Authorization": "Bearer token",
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Headers: http.Header{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "header set as list",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Headers map[string][]any
|
|
}{
|
|
Headers: map[string][]any{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Headers: http.Header{
|
|
"Authorization": {"Bearer token"},
|
|
},
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "method set",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Method string
|
|
}{
|
|
Method: http.MethodPost,
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Method: http.MethodPost,
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "body set",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
Body struct{ Id string }
|
|
}{
|
|
Body: struct{ Id string }{
|
|
Id: "asdf123",
|
|
},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{
|
|
Body: bytes.NewReader([]byte(`{"id":"asdf123"}`)),
|
|
},
|
|
wantErr: func(err error) bool {
|
|
return err == nil
|
|
},
|
|
},
|
|
{
|
|
name: "invalid header",
|
|
args: args{
|
|
arg: runtime.ToValue(
|
|
&struct {
|
|
NotExists struct{}
|
|
}{
|
|
NotExists: struct{}{},
|
|
},
|
|
).ToObject(runtime),
|
|
},
|
|
wantConfig: fetchConfig{},
|
|
wantErr: func(err error) bool {
|
|
return zerrors.IsErrorInvalidArgument(err)
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &HTTP{
|
|
runtime: runtime,
|
|
client: http.DefaultClient,
|
|
}
|
|
gotConfig := new(fetchConfig)
|
|
|
|
err := c.fetchConfigFromArg(tt.args.arg, gotConfig)
|
|
if !tt.wantErr(err) {
|
|
t.Errorf("HTTP.fetchConfigFromArg() error = %v", err)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(gotConfig.Headers, tt.wantConfig.Headers) {
|
|
t.Errorf("config.Headers got = %#v, want %#v", gotConfig.Headers, tt.wantConfig.Headers)
|
|
}
|
|
if gotConfig.Method != tt.wantConfig.Method {
|
|
t.Errorf("config.Method got = %#v, want %#v", gotConfig.Method, tt.wantConfig.Method)
|
|
}
|
|
|
|
if tt.wantConfig.Body == nil {
|
|
if gotConfig.Body != nil {
|
|
t.Errorf("didn't expect a body")
|
|
}
|
|
return
|
|
}
|
|
|
|
gotBody, _ := io.ReadAll(gotConfig.Body)
|
|
wantBody, _ := io.ReadAll(tt.wantConfig.Body)
|
|
|
|
if !reflect.DeepEqual(gotBody, wantBody) {
|
|
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTP_buildHTTPRequest(t *testing.T) {
|
|
runtime := goja.New()
|
|
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
|
|
|
type args struct {
|
|
args []goja.Value
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantReq *http.Request
|
|
shouldPanic bool
|
|
}{
|
|
{
|
|
name: "only url",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "no params",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(&struct{}{}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "overwrite headers",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Headers map[string][]interface{}
|
|
}{
|
|
Headers: map[string][]interface{}{"Authorization": {"some token"}},
|
|
}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: http.Header{
|
|
"Authorization": []string{"some token"},
|
|
},
|
|
Body: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "post with body",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Body struct{ MyData string }
|
|
}{
|
|
Body: struct{ MyData string }{MyData: "hello world"},
|
|
}),
|
|
},
|
|
},
|
|
wantReq: &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: mustNewURL(t, "http://my-url.ch"),
|
|
Header: defaultFetchConfig.Headers,
|
|
Body: io.NopCloser(bytes.NewReader([]byte(`{"myData":"hello world"}`))),
|
|
},
|
|
},
|
|
{
|
|
name: "too many args",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue("http://my-url.ch"),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "no args",
|
|
args: args{
|
|
args: []goja.Value{},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "invalid config",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Invalid bool
|
|
}{
|
|
Invalid: true,
|
|
}),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
{
|
|
name: "invalid method",
|
|
args: args{
|
|
args: []goja.Value{
|
|
runtime.ToValue("http://my-url.ch"),
|
|
runtime.ToValue(struct {
|
|
Method string
|
|
}{
|
|
Method: " asdf asdf",
|
|
}),
|
|
},
|
|
},
|
|
wantReq: nil,
|
|
shouldPanic: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
panicked := false
|
|
if tt.shouldPanic {
|
|
defer func() {
|
|
if panicked != tt.shouldPanic {
|
|
t.Errorf("wanted panic: %v got %v", tt.shouldPanic, panicked)
|
|
}
|
|
}()
|
|
defer func() {
|
|
recover()
|
|
panicked = true
|
|
}()
|
|
}
|
|
|
|
c := &HTTP{
|
|
runtime: runtime,
|
|
}
|
|
|
|
gotReq := c.buildHTTPRequest(context.Background(), tt.args.args)
|
|
|
|
if tt.shouldPanic {
|
|
return
|
|
}
|
|
|
|
if gotReq.URL.String() != tt.wantReq.URL.String() {
|
|
t.Errorf("url = %s, want %s", gotReq.URL, tt.wantReq.URL)
|
|
}
|
|
|
|
if !reflect.DeepEqual(gotReq.Header, tt.wantReq.Header) {
|
|
t.Errorf("headers = %v, want %v", gotReq.Header, tt.wantReq.Header)
|
|
}
|
|
|
|
if gotReq.Method != tt.wantReq.Method {
|
|
t.Errorf("method = %s, want %s", gotReq.Method, tt.wantReq.Method)
|
|
}
|
|
|
|
if tt.wantReq.Body == nil {
|
|
if gotReq.Body != nil {
|
|
t.Errorf("didn't expect a body")
|
|
}
|
|
return
|
|
}
|
|
|
|
gotBody, _ := io.ReadAll(gotReq.Body)
|
|
wantBody, _ := io.ReadAll(tt.wantReq.Body)
|
|
|
|
if !reflect.DeepEqual(gotBody, wantBody) {
|
|
t.Errorf("config.Body got = %s, want %s", gotBody, wantBody)
|
|
}
|
|
})
|
|
}
|
|
}
|