Feat #43: Add CLI command for the backends #11
3 changed files with 348 additions and 0 deletions
|
@ -6,8 +6,18 @@ import (
|
||||||
|
|
||||||
"github.com/dexidp/dex/connector"
|
"github.com/dexidp/dex/connector"
|
||||||
"github.com/dexidp/dex/pkg/log"
|
"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{}
|
type RefuseAllConfig struct{}
|
||||||
|
|
||||||
func (c *RefuseAllConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
|
func (c *RefuseAllConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
|
||||||
|
|
123
polyculeconnect/services/backend/backend.go
Normal file
123
polyculeconnect/services/backend/backend.go
Normal file
|
@ -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}
|
||||||
|
}
|
215
polyculeconnect/services/backend/backend_test.go
Normal file
215
polyculeconnect/services/backend/backend_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue