Skip to content

Commit

Permalink
feat: add connector webhook logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Palesandro committed Nov 9, 2023
1 parent e41a28b commit b698b31
Show file tree
Hide file tree
Showing 17 changed files with 957 additions and 6 deletions.
4 changes: 4 additions & 0 deletions cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"golang.org/x/crypto/bcrypt"

"github.com/dexidp/dex/pkg/log"
"github.com/dexidp/dex/pkg/webhook/config"

Check failure on line 14 in cmd/dex/config.go

View workflow job for this annotation

GitHub Actions / Lint

could not import github.com/dexidp/dex/pkg/webhook/config (-: # github.com/dexidp/dex/pkg/webhook/config
"github.com/dexidp/dex/server"
"github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/ent"
Expand Down Expand Up @@ -49,6 +50,9 @@ type Config struct {
// querying the storage. Cannot be specified without enabling a passwords
// database.
StaticPasswords []password `json:"staticPasswords"`

// ConnectorFilterHooks is a list of hooks that can be used to filter the connectors`
ConnectorFilterHooks config.ConnectorFilterHooks `json:"connectorFiltersHooks"`
}

// Validate the configuration
Expand Down
1 change: 1 addition & 0 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func runServe(options serveOptions) error {
Now: now,
PrometheusRegistry: prometheusRegistry,
HealthChecker: healthChecker,
ConnectorFilterHooks: c.ConnectorFilterHooks,
}
if c.Expiry.SigningKeys != "" {
signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/stretchr/testify v1.8.4
go.etcd.io/etcd/client/pkg/v3 v3.5.9
go.etcd.io/etcd/client/v3 v3.5.9
go.uber.org/mock v0.3.0
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741
golang.org/x/net v0.17.0
Expand Down Expand Up @@ -88,7 +89,7 @@ require (
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U=
Expand All @@ -251,8 +253,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
14 changes: 14 additions & 0 deletions pkg/webhook/config/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package config

type WebhookConfig struct {
URL string `json:"url"`
InsecureSkipVerify bool `json:"insecureSkipVerify"`
TLSRootCAFile string `json:"tlsRootCAFile"`
ClientAuthentication *ClientAuthentication `json:"clientAuthentication"`
}

type ClientAuthentication struct {
ClientCertificateFile string `json:"clientCertificateFile"`
ClientKeyFile string `json:"clientKeyFile"`
ClientCAFile string `json:"clientCAFile"`
}
24 changes: 24 additions & 0 deletions pkg/webhook/config/connectortypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package config

// HookRequestScope is the context of the request
type HookRequestScope struct {
// Headers is the headers of the request
Headers []string `json:"headers"`
// Params is the params of the request
Params []string `json:"params"`
}

type ConnectorFilterHook struct {
// Name is the name of the webhook
Name string `json:"name"`
// To be modified to enum?
Type HookType `json:"type"`

Check failure on line 15 in pkg/webhook/config/connectortypes.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: HookType (typecheck)

Check failure on line 15 in pkg/webhook/config/connectortypes.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: HookType) (typecheck)

Check failure on line 15 in pkg/webhook/config/connectortypes.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: HookType) (typecheck)

Check failure on line 15 in pkg/webhook/config/connectortypes.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: HookType) (typecheck)

Check failure on line 15 in pkg/webhook/config/connectortypes.go

View workflow job for this annotation

GitHub Actions / Test

undefined: HookType
// RequestScope is the context of the request
RequestScope *HookRequestScope `json:"requestContext"`
// Config is the configuration of the webhook
Config *WebhookConfig `json:"config"`
}

type ConnectorFilterHooks struct {
FilterHooks []*ConnectorFilterHook `json:"filterHooks"`
}
74 changes: 74 additions & 0 deletions pkg/webhook/connectors/connectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:generate go run -mod mod go.uber.org/mock/mockgen -destination=./mocks/mock_caller.go -package=connectors --source=types.go FilterCaller
package connectors

import (
"encoding/json"
"fmt"
"net/http"

"github.com/dexidp/dex/pkg/webhook/config"

Check failure on line 9 in pkg/webhook/connectors/connectors.go

View workflow job for this annotation

GitHub Actions / Lint

could not import github.com/dexidp/dex/pkg/webhook/config (-: # github.com/dexidp/dex/pkg/webhook/config
"github.com/dexidp/dex/pkg/webhook/helpers"
"github.com/dexidp/dex/storage"
)

func NewConnectorFilter(hook *config.ConnectorFilterHook) (*ConnectorFilterHook, error) {
var hookInvoker FilterCaller
switch hook.Type {
case config.External:
h, err := helpers.NewWebhookHTTPHelpers(hook.Config)
if err != nil {
return nil, fmt.Errorf("could not create webhook http helpers: %w", err)
}
hookInvoker = NewFilterCaller(h, hook.RequestScope)
default:
return nil, fmt.Errorf("unknown type: %s", hook.Type)
}
return &ConnectorFilterHook{
Name: hook.Name,
FilterInvoker: hookInvoker,
}, nil
}

func (f WebhookFilterCaller) callHook(connectors []ConnectorContext, req RequestContext) ([]ConnectorContext, error) {
toMarshal := FilterWebhookPayload{
Connectors: connectors,
Request: req,
}

payload, err := json.Marshal(toMarshal)
if err != nil {
return nil, fmt.Errorf("could not serialize claims: %w", err)
}

body, err := f.transportHelper.CallWebhook(payload)
if err != nil {
return nil, fmt.Errorf("could not call webhook: %w", err)
}
var res []ConnectorContext

if err := json.Unmarshal(body, &res); err != nil {
return nil, fmt.Errorf("could not unmarshal response: %w", err)
}

return res, nil
}

func NewFilterCaller(h helpers.WebhookHTTPHelpers,
context *config.HookRequestScope,
) *WebhookFilterCaller {
return &WebhookFilterCaller{
RequestScope: context,
transportHelper: h,
}
}

func (f WebhookFilterCaller) CallHook(connectors []storage.Connector,
r *http.Request,
) ([]storage.Connector, error) {
payload := createConnectorWebhookPayload(f.RequestScope, connectors, r)
filteredConnectors, err := f.callHook(payload.Connectors, payload.Request)
if err != nil {
return nil, err
}
return unwrapConnectorWebhookPayload(filteredConnectors, connectors), nil
}
111 changes: 111 additions & 0 deletions pkg/webhook/connectors/connectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package connectors

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"

"github.com/dexidp/dex/pkg/webhook/config"
"github.com/dexidp/dex/pkg/webhook/helpers"
"github.com/dexidp/dex/storage"
)

func TestNewConnectorFilter(t *testing.T) {
d, err := NewConnectorFilter(&config.ConnectorFilterHook{
Name: "test",
Type: config.External,
RequestScope: &config.HookRequestScope{
Headers: []string{"header1", "header2"},
Params: []string{"param1", "param2"},
},
Config: &config.WebhookConfig{
URL: "http://test.com",
InsecureSkipVerify: true,
},
})
assert.NoError(t, err)
assert.NotNil(t, d)
assert.Equal(t, d.Name, "test")
assert.IsType(t, d.FilterInvoker, &WebhookFilterCaller{})
}

func TestNewConnectorFilter_UnknownType(t *testing.T) {
d, err := NewConnectorFilter(&config.ConnectorFilterHook{
Name: "test",
Type: "unknown",
RequestScope: &config.HookRequestScope{
Headers: []string{"header1", "header2"},
Params: []string{"param1", "param2"},
},
Config: &config.WebhookConfig{
URL: "http://test.com",
InsecureSkipVerify: true,
},
})
assert.Error(t, err)
assert.Nil(t, d)
}

func TestNewFilterCaller(t *testing.T) {
h, err := helpers.NewWebhookHTTPHelpers(&config.WebhookConfig{
URL: "http://test.com",
InsecureSkipVerify: true,
})
assert.NoError(t, err)
d := NewFilterCaller(h, &config.HookRequestScope{
Headers: []string{"header1", "header2"},
Params: []string{"param1", "param2"},
})
assert.NotNil(t, d)
assert.Equal(t, h, d.transportHelper)
assert.Equal(t, d.RequestScope.Headers, []string{"header1", "header2"})
assert.Equal(t, d.RequestScope.Params, []string{"param1", "param2"})
}

func TestCallHook_Logic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
h := helpers.NewMockWebhookHTTPHelpers(ctrl)
h.EXPECT().CallWebhook(gomock.Any()).Return([]byte(`[{"id": "test", "type": "test", "name": "test"}]`), nil)
d := NewFilterCaller(h, &config.HookRequestScope{
Headers: []string{"header1", "header2"},
Params: []string{"param1", "param2"},
})
connectorList, err := d.CallHook([]storage.Connector{
{
ID: "test",
Type: "test",
Name: "test",
},
}, &http.Request{})
assert.NoError(t, err)
assert.Equal(t, connectorList, []storage.Connector{
{
ID: "test",
Type: "test",
Name: "test",
},
})
}

func TestCallHook_Logic_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
h := helpers.NewMockWebhookHTTPHelpers(ctrl)
h.EXPECT().CallWebhook(gomock.Any()).Return(nil, assert.AnError)
d := NewFilterCaller(h, &config.HookRequestScope{
Headers: []string{"header1", "header2"},
Params: []string{"param1", "param2"},
})
connectorList, err := d.CallHook([]storage.Connector{
{
ID: "test",
Type: "test",
Name: "test",
},
}, &http.Request{})
assert.Error(t, err)
assert.Nil(t, connectorList)
}
55 changes: 55 additions & 0 deletions pkg/webhook/connectors/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package connectors

import (
"net/http"

"golang.org/x/exp/slices"

"github.com/dexidp/dex/pkg/webhook/config"
"github.com/dexidp/dex/storage"
)

func createConnectorWebhookPayload(requestScope *config.HookRequestScope, connectors []storage.Connector,
r *http.Request,
) *FilterWebhookPayload {
payload := &FilterWebhookPayload{
Connectors: []ConnectorContext{},
Request: RequestContext{},
}
for _, c := range connectors {
payload.Connectors = append(payload.Connectors, ConnectorContext{
ID: c.ID,
Type: c.Type,
Name: c.Name,
})
}
payload.Request.Params = make(map[string][]string)
if r != nil && r.URL != nil {
for k, v := range r.URL.Query() {
if slices.Contains(requestScope.Params, k) {
payload.Request.Params[k] = v
}
}
}
payload.Request.Headers = make(map[string][]string)
for k, v := range r.Header {
if slices.Contains(requestScope.Headers, k) {
payload.Request.Headers[k] = v
}
}
return payload
}

func unwrapConnectorWebhookPayload(filteredConnectors []ConnectorContext,
initialConnectors []storage.Connector,
) []storage.Connector {
mappedConnectors := make([]storage.Connector, 0)
for _, filteredConnector := range filteredConnectors {
for _, initialConnector := range initialConnectors {
if filteredConnector.ID == initialConnector.ID {
mappedConnectors = append(mappedConnectors, initialConnector)
}
}
}
return mappedConnectors
}
Loading

0 comments on commit b698b31

Please sign in to comment.