From 550622e512fb3a560b646a86005a269a077641aa Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sun, 29 Oct 2023 13:29:48 +0100 Subject: [PATCH] feat #43: add service to handle backends in the storage --- polyculeconnect/connector/refuse_all.go | 10 + polyculeconnect/services/backend/backend.go | 123 ++++++++++ .../services/backend/backend_test.go | 215 ++++++++++++++++++ 3 files changed, 348 insertions(+) create mode 100644 polyculeconnect/services/backend/backend.go create mode 100644 polyculeconnect/services/backend/backend_test.go diff --git a/polyculeconnect/connector/refuse_all.go b/polyculeconnect/connector/refuse_all.go index edb862c..5e13970 100644 --- a/polyculeconnect/connector/refuse_all.go +++ b/polyculeconnect/connector/refuse_all.go @@ -6,8 +6,18 @@ import ( "github.com/dexidp/dex/connector" "github.com/dexidp/dex/pkg/log" + "github.com/dexidp/dex/storage" ) +const TypeRefuseAll = "refuseAll" + +var RefuseAllConnectorConfig storage.Connector = storage.Connector{ + ID: "null", + Name: "RefuseAll", + Type: TypeRefuseAll, + Config: nil, +} + type RefuseAllConfig struct{} func (c *RefuseAllConfig) Open(id string, logger log.Logger) (connector.Connector, error) { diff --git a/polyculeconnect/services/backend/backend.go b/polyculeconnect/services/backend/backend.go new file mode 100644 index 0000000..9f9d6f5 --- /dev/null +++ b/polyculeconnect/services/backend/backend.go @@ -0,0 +1,123 @@ +package backend + +import ( + "encoding/json" + "errors" + "fmt" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector" + "github.com/dexidp/dex/connector/oidc" + "github.com/dexidp/dex/storage" +) + +var ErrUnsupportedType = errors.New("unsupported connector type") + +type BackendConfig struct { + ID string + Name string + Issuer string + ClientID string + ClientSecret string + RedirectURI string +} + +func (bc *BackendConfig) OIDC() oidc.Config { + return oidc.Config{ + Issuer: bc.Issuer, + ClientID: bc.ClientID, + ClientSecret: bc.ClientSecret, + RedirectURI: bc.RedirectURI, + } +} + +func (bc *BackendConfig) Storage() (storage.Connector, error) { + oidcJSON, err := json.Marshal(bc.OIDC()) + if err != nil { + return storage.Connector{}, fmt.Errorf("failed to serialize oidc config: %w", err) + } + + return storage.Connector{ + ID: bc.ID, + Type: "oidc", + Name: bc.Name, + Config: oidcJSON, + }, nil +} + +func (bc *BackendConfig) FromConnector(connector storage.Connector) error { + var oidc oidc.Config + if connector.Type != "oidc" { + return ErrUnsupportedType + } + if err := json.Unmarshal(connector.Config, &oidc); err != nil { + return fmt.Errorf("invalid OIDC config: %w", err) + } + bc.ID = connector.ID + bc.Name = connector.Name + bc.ClientID = oidc.ClientID + bc.ClientSecret = oidc.ClientSecret + bc.Issuer = oidc.Issuer + bc.RedirectURI = oidc.RedirectURI + return nil +} + +type Service interface { + ListBackends() ([]BackendConfig, error) + GetBackend(id string) (BackendConfig, error) + AddBackend(config BackendConfig) error + RemoveBackend(id string) error +} + +type concreteBackendService struct { + s storage.Storage +} + +func (cbs *concreteBackendService) ListBackends() ([]BackendConfig, error) { + connectors, err := cbs.s.ListConnectors() + if err != nil { + return nil, fmt.Errorf("failed to get connectors from storage: %w", err) + } + var res []BackendConfig + for _, c := range connectors { + + // We know that this type is special, we don't want to use it at all here + if c.Type == connector.TypeRefuseAll { + continue + } + + var b BackendConfig + if err := b.FromConnector(c); err != nil { + return res, err + } + res = append(res, b) + } + return res, nil +} + +func (cbs *concreteBackendService) GetBackend(connectorID string) (BackendConfig, error) { + c, err := cbs.s.GetConnector(connectorID) + if err != nil { + return BackendConfig{}, fmt.Errorf("failed to get connector from storage: %w", err) + } + var res BackendConfig + if err := res.FromConnector(c); err != nil { + return BackendConfig{}, err + } + return res, nil +} + +func (cbs *concreteBackendService) AddBackend(config BackendConfig) error { + storageConf, err := config.Storage() + if err != nil { + return fmt.Errorf("failed to create storage configuration: %w", err) + } + return cbs.s.CreateConnector(storageConf) +} + +func (cbs *concreteBackendService) RemoveBackend(connectorID string) error { + return cbs.s.DeleteConnector(connectorID) +} + +func New(s storage.Storage) Service { + return &concreteBackendService{s} +} diff --git a/polyculeconnect/services/backend/backend_test.go b/polyculeconnect/services/backend/backend_test.go new file mode 100644 index 0000000..4fde675 --- /dev/null +++ b/polyculeconnect/services/backend/backend_test.go @@ -0,0 +1,215 @@ +package backend_test + +import ( + "encoding/json" + "fmt" + "testing" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend" + "github.com/dexidp/dex/storage" + "github.com/dexidp/dex/storage/memory" + logt "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testDomain string = "https://test.domain.com" + testClientID string = "this_is_an_id" + testClientSecret string = "this_is_a_secret" + testRedirectURI string = "http://127.0.0.1:5000/callback" + testConnectorName string = "Test connector" +) + +func generateConnector(id string) storage.Connector { + confJson := fmt.Sprintf(`{ + "issuer": "%s", + "clientID": "%s", + "clientSecret": "%s", + "redirectURI": "%s" +}`, testDomain, testClientID, testClientSecret, testRedirectURI) + storageConfig := storage.Connector{ + ID: id, + Name: testConnectorName, + Type: "oidc", + Config: []byte(confJson), + } + return storageConfig +} + +func generateConfig(id string) backend.BackendConfig { + return backend.BackendConfig{ + ID: id, + Name: testConnectorName, + Issuer: testDomain, + ClientID: testClientID, + ClientSecret: testClientSecret, + RedirectURI: testRedirectURI, + } +} + +func checkStrInMap(t *testing.T, vals map[string]interface{}, key, expected string) { + rawVal, ok := vals[key] + require.Truef(t, ok, "missing key %s", key) + strVal, ok := rawVal.(string) + require.Truef(t, ok, "invalid string format %v", rawVal) + assert.Equal(t, expected, strVal, "unexpected value") +} + +func initStorage(t *testing.T) storage.Storage { + logger, _ := logt.NewNullLogger() + s := memory.New(logger) + + require.NoError(t, s.CreateConnector(connector.RefuseAllConnectorConfig)) + require.NoError(t, s.CreateConnector(generateConnector("test0"))) + require.NoError(t, s.CreateConnector(generateConnector("test1"))) + + return s +} + +func TestBackendConfigFromConnector(t *testing.T) { + connector := generateConnector("test") + var bc backend.BackendConfig + require.NoError(t, bc.FromConnector(connector)) + + assert.Equal(t, testDomain, bc.Issuer) + assert.Equal(t, testClientID, bc.ClientID) + assert.Equal(t, testClientSecret, bc.ClientSecret) + assert.Equal(t, testRedirectURI, bc.RedirectURI) + assert.Equal(t, testConnectorName, bc.Name) + assert.Equal(t, "test", bc.ID) +} + +func TestBackendConfigInvalidType(t *testing.T) { + connector := generateConnector("test") + connector.Type = "test" + var bc backend.BackendConfig + assert.ErrorIs(t, bc.FromConnector(connector), backend.ErrUnsupportedType) +} + +func TestBackendConfigInvalidOIDCConfig(t *testing.T) { + connector := generateConnector("test") + connector.Config = []byte("toto") + var bc backend.BackendConfig + assert.ErrorContains(t, bc.FromConnector(connector), "invalid OIDC config") +} + +func TestOIDCConfigFromBackendConfig(t *testing.T) { + conf := generateConfig("test") + oidcConf := conf.OIDC() + + assert.Equal(t, testDomain, oidcConf.Issuer) + assert.Equal(t, testClientID, oidcConf.ClientID) + assert.Equal(t, testClientSecret, oidcConf.ClientSecret) + assert.Equal(t, testRedirectURI, oidcConf.RedirectURI) +} + +func TestConnectorConfigFromBackendConfig(t *testing.T) { + conf := generateConfig("test") + con, err := conf.Storage() + require.NoError(t, err) + + // The OIDC config is stored as JSON data, we just want the raw keys here + var oidcConf map[string]interface{} + require.NoError(t, json.Unmarshal(con.Config, &oidcConf)) + + assert.Equal(t, "oidc", con.Type) + assert.Equal(t, "test", con.ID) + assert.Equal(t, testConnectorName, con.Name) + checkStrInMap(t, oidcConf, "issuer", testDomain) + checkStrInMap(t, oidcConf, "clientID", testClientID) + checkStrInMap(t, oidcConf, "clientSecret", testClientSecret) + checkStrInMap(t, oidcConf, "redirectURI", testRedirectURI) +} + +func TestListBackendsEmpty(t *testing.T) { + logger, _ := logt.NewNullLogger() + s := memory.New(logger) + + // add the default refuse all connector, it should not be visible in the list + require.NoError(t, s.CreateConnector(connector.RefuseAllConnectorConfig)) + srv := backend.New(s) + + backends, err := srv.ListBackends() // empty list, and no error + require.NoError(t, err) + require.Len(t, backends, 0) +} + +func TestListBackendsNotEmpty(t *testing.T) { + s := initStorage(t) + srv := backend.New(s) + + backends, err := srv.ListBackends() // empty list, and no error + expectedIds := []string{"test0", "test1"} + require.NoError(t, err) + assert.Len(t, backends, 2) + for _, c := range backends { + assert.Contains(t, expectedIds, c.ID) + } +} + +func TestGetBackend(t *testing.T) { + s := initStorage(t) + srv := backend.New(s) + + t.Run("OK", func(t *testing.T) { + conf, err := srv.GetBackend("test0") + require.NoError(t, err) + assert.Equal(t, testDomain, conf.Issuer) + assert.Equal(t, testClientID, conf.ClientID) + assert.Equal(t, testClientSecret, conf.ClientSecret) + assert.Equal(t, testRedirectURI, conf.RedirectURI) + assert.Equal(t, testConnectorName, conf.Name) + assert.Equal(t, "test0", conf.ID) + }) + + t.Run("Not exist", func(t *testing.T) { + _, err := srv.GetBackend("toto") + assert.ErrorIs(t, err, storage.ErrNotFound) + }) + + t.Run("Invalid type", func(t *testing.T) { + _, err := srv.GetBackend("null") // null has a RefuseAll type, which is unsupported here + assert.ErrorIs(t, err, backend.ErrUnsupportedType) + }) +} + +func TestAddBackend(t *testing.T) { + s := initStorage(t) + srv := backend.New(s) + + t.Run("OK", func(t *testing.T) { + conf := generateConfig("test_add") + require.NoError(t, srv.AddBackend(conf)) + + var parsedConf backend.BackendConfig + storageConf, err := s.GetConnector("test_add") + require.NoError(t, err) + require.NoError(t, parsedConf.FromConnector(storageConf)) + assert.Equal(t, conf, parsedConf) + }) + + t.Run("Already exists", func(t *testing.T) { + require.ErrorIs(t, srv.AddBackend(generateConfig("test0")), storage.ErrAlreadyExists) + }) +} + +func TestRemoveBackend(t *testing.T) { + s := initStorage(t) + srv := backend.New(s) + + t.Run("OK", func(t *testing.T) { + require.NoError(t, srv.AddBackend(generateConfig("to_remove"))) + _, err := s.GetConnector("to_remove") + require.NoError(t, err) // no error means it's present + + require.NoError(t, srv.RemoveBackend("to_remove")) + _, err = s.GetConnector("to_remove") + assert.ErrorIs(t, err, storage.ErrNotFound) // means it's been deleted + }) + + t.Run("No present", func(t *testing.T) { + require.ErrorIs(t, srv.RemoveBackend("toto"), storage.ErrNotFound) + }) +}