Compare commits

...

4 commits

Author SHA1 Message Date
9d2d49425d chore: refactor serve command
All checks were successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2023-11-08 19:32:56 +01:00
69a07ce076 feat #43: add cli command to manage backends 2023-11-08 19:32:56 +01:00
550622e512 feat #43: add service to handle backends in the storage 2023-11-08 19:32:56 +01:00
e4497cad8c Add envrc to facilitate development 2023-11-08 19:32:56 +01:00
15 changed files with 672 additions and 27 deletions

18
.envrc Normal file
View file

@ -0,0 +1,18 @@
# Can be debug,info,warning,error
export LOG_LEVEL=debug
# Can be net,unix
export SERVER_MODE=net
export SERVER_HOST="0.0.0.0"
export SERVER_PORT="5000"
# SERVER_SOCK_PATH = ""
export STORAGE_TYPE="sqlite"
export STORAGE_FILEPATH="./build/polyculeconnect.db"
# STORAGE_HOST = "127.0.0.1"
# STORAGE_PORT = "5432"
# STORAGE_DB = "polyculeconnect"
# STORAGE_USER = "polyculeconnect"
# STORAGE_PASSWORD = "polyculeconnect"
# STORAGE_SSL_MODE = "disable"
# STORAGE_SSL_CA_FILE = ""

View file

@ -0,0 +1,66 @@
package cmd
import (
"fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend"
"github.com/spf13/cobra"
)
var (
backendID string
backendName string
backendIssuer string
)
var backendAddCmd = &cobra.Command{
Use: "add",
Short: "Add a new backend to the storage",
Long: `Add a new backend to the storage.
Parameters to provide:
- id: Unique ID to represent the backend in the storage
- name: Human readable name to represent the backend. It will be used by
the user in the authentication page to select a backend during
authentication
- issuer: Full hostname of the OIDC provider, e.g. 'https://github.com'`,
Run: func(cmd *cobra.Command, args []string) {
addNewBackend()
},
}
func addNewBackend() {
c := utils.InitConfig("")
s := utils.InitStorage(c)
clientID, clientSecret, err := services.GenerateClientIDSecret()
if err != nil {
utils.Failf("Failed to generate client id or secret: %s", err.Error())
}
backendConf := backend.BackendConfig{
Issuer: backendIssuer,
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURI: c.RedirectURI(),
ID: backendID,
Name: backendName,
}
if err := backend.New(s).AddBackend(backendConf); err != nil {
utils.Failf("Failed to add new backend to storage: %s", err.Error())
}
fmt.Printf("New backend %s added.\n", backendName)
printProperty("Client ID", clientID)
printProperty("Client secret", clientSecret)
}
func init() {
backendCmd.AddCommand(backendAddCmd)
backendAddCmd.Flags().StringVarP(&backendID, "id", "i", "", "ID to identify the backend in the storage")
backendAddCmd.Flags().StringVarP(&backendName, "name", "n", "", "Name to represent the backend")
backendAddCmd.Flags().StringVarP(&backendIssuer, "issuer", "d", "", "Full hostname of the backend")
}

View file

@ -0,0 +1,30 @@
package cmd
import (
"fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
"github.com/spf13/cobra"
)
var backendCmd = &cobra.Command{
Use: "backend",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("backend called")
},
}
func printProperty(key, value string) {
fmt.Printf("\t- %s: %s\n", key, value)
}
func init() {
cmd.RootCmd.AddCommand(backendCmd)
}

View file

@ -0,0 +1,38 @@
package cmd
import (
"errors"
"fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend"
"github.com/dexidp/dex/storage"
"github.com/spf13/cobra"
)
var backendRemoveCmd = &cobra.Command{
Use: "remove <backend_id>",
Short: "Remove a backend",
Long: `Remove the backend with the given ID from the database.
If the backend is not found in the database, no error is returned`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
removeBackend(args[0])
},
}
func removeBackend(backendID string) {
s := utils.InitStorage(utils.InitConfig(""))
if err := backend.New(s).RemoveBackend(backendID); err != nil {
if !errors.Is(err, storage.ErrNotFound) {
utils.Failf("Failed to remove backend: %s", err.Error())
}
}
fmt.Println("Backend deleted")
}
func init() {
backendCmd.AddCommand(backendRemoveCmd)
}

View file

@ -0,0 +1,67 @@
package cmd
import (
"errors"
"fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend"
"github.com/dexidp/dex/storage"
"github.com/spf13/cobra"
)
var backendShowCmd = &cobra.Command{
Use: "show [backend_id]",
Short: "Display installed backends",
Long: `Display the configuration for the backends.
Pass the commands without arguments to display the list of currently installed backends
Pass the optional 'id' argument to display the configuration for this specific backend`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
s := utils.InitStorage(utils.InitConfig(""))
if len(args) > 0 {
showBackend(args[0], backend.New(s))
} else {
listBackends(backend.New(s))
}
},
}
func showBackend(backendId string, backendService backend.Service) {
backendConfig, err := backendService.GetBackend(backendId)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
utils.Failf("Backend with ID %s does not exist\n", backendId)
}
utils.Failf("Failed to get config for backend %s: %q\n", backendId, err.Error())
}
fmt.Println("Backend config:")
printProperty("ID", backendConfig.ID)
printProperty("Name", backendConfig.Name)
printProperty("Issuer", backendConfig.Issuer)
printProperty("Client ID", backendConfig.ClientID)
printProperty("Client secret", backendConfig.ClientSecret)
printProperty("Redirect URI", backendConfig.RedirectURI)
}
func listBackends(backendService backend.Service) {
backends, err := backendService.ListBackends()
if err != nil {
utils.Failf("Failed to list backends: %q\n", err.Error())
}
if len(backends) == 0 {
fmt.Println("No backend configured")
return
}
for _, b := range backends {
fmt.Printf("\t - %s: (%s) - %s\n", b.ID, b.Name, b.Issuer)
}
}
func init() {
backendCmd.AddCommand(backendShowCmd)
}

View file

@ -6,8 +6,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// rootCmd represents the base command when called without any subcommands // RootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
Use: "polyculeconnect", Use: "polyculeconnect",
Short: "You're in their DMs, I'm in their SSO", Short: "You're in their DMs, I'm in their SSO",
Long: `PolyculeConnect is a SSO OpenIDConnect provider which allows multiple authentication backends, Long: `PolyculeConnect is a SSO OpenIDConnect provider which allows multiple authentication backends,
@ -20,7 +20,7 @@ and enables authentication federation among several infrastructures.`,
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd. // This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() { func Execute() {
err := rootCmd.Execute() err := RootCmd.Execute()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
@ -38,5 +38,5 @@ func init() {
// rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
// Disable the default `completion` command to generate the autocompletion files // Disable the default `completion` command to generate the autocompletion files
rootCmd.Root().CompletionOptions.DisableDefaultCmd = true RootCmd.Root().CompletionOptions.DisableDefaultCmd = true
} }

View file

@ -1,4 +1,4 @@
package cmd package serve
import ( import (
"context" "context"
@ -6,12 +6,12 @@ import (
"os/signal" "os/signal"
"time" "time"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/server" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/server"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services"
"github.com/dexidp/dex/connector/oidc"
dex_server "github.com/dexidp/dex/server" dex_server "github.com/dexidp/dex/server"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -35,18 +35,11 @@ variables`,
func serve() { func serve() {
mainCtx, cancel := context.WithCancel(context.Background()) mainCtx, cancel := context.WithCancel(context.Background())
conf, err := config.New(configPath) conf := utils.InitConfig(configPath)
if err != nil {
panic(err)
}
logger.Init(conf.LogLevel) logger.Init(conf.LogLevel)
logger.L.Infof("Initialized logger with level %v", conf.LogLevel) logger.L.Infof("Initialized logger with level %v", conf.LogLevel)
storageType, err := services.InitStorage(conf) storageType := utils.InitStorage(conf)
if err != nil {
logger.L.Fatalf("Failed to initialize storage backend: %s", err.Error())
}
logger.L.Infof("Initialized storage backend %q", conf.StorageType) logger.L.Infof("Initialized storage backend %q", conf.StorageType)
dexConf := dex_server.Config{ dexConf := dex_server.Config{
Web: dex_server.WebConfig{ Web: dex_server.WebConfig{
@ -64,7 +57,7 @@ func serve() {
logger.L.Info("Initializing authentication backends") logger.L.Info("Initializing authentication backends")
dex_server.ConnectorsConfig["refuseAll"] = func() dex_server.ConnectorConfig { return new(connector.RefuseAllConfig) } dex_server.ConnectorsConfig[connector.TypeRefuseAll] = func() dex_server.ConnectorConfig { return new(connector.RefuseAllConfig) }
connectors, err := dexConf.Storage.ListConnectors() connectors, err := dexConf.Storage.ListConnectors()
if err != nil { if err != nil {
logger.L.Fatalf("Failed to get existing connectors: %s", err.Error()) logger.L.Fatalf("Failed to get existing connectors: %s", err.Error())
@ -74,14 +67,7 @@ func serve() {
connectorIDs = append(connectorIDs, conn.ID) connectorIDs = append(connectorIDs, conn.ID)
} }
backend := config.BackendConfig{ if err := services.AddDefaultBackend(storageType); err != nil {
Config: &oidc.Config{},
Name: "RefuseAll",
ID: "null",
Type: "refuseAll",
}
if err := services.CreateConnector(&backend, &dexConf, connectorIDs); err != nil {
logger.L.Errorf("Failed to add connector for backend RefuseAll to stage: %s", err.Error()) logger.L.Errorf("Failed to add connector for backend RefuseAll to stage: %s", err.Error())
} }
@ -140,7 +126,7 @@ func serve() {
} }
func init() { func init() {
rootCmd.AddCommand(serveCmd) cmd.RootCmd.AddCommand(serveCmd)
// Here you will define your flags and configuration settings. // Here you will define your flags and configuration settings.

View file

@ -0,0 +1,40 @@
package utils
import (
"fmt"
"os"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services"
"github.com/dexidp/dex/storage"
)
// Fail displays the given error to stderr and exits the program with a returncode 1
func Fail(errMsg string) {
fmt.Fprintln(os.Stderr, errMsg)
os.Exit(1)
}
// Fail displays the given formatted error to stderr and exits the program with a returncode 1
func Failf(msg string, args ...any) {
fmt.Fprintf(os.Stderr, msg+"\n", args...)
os.Exit(1)
}
// InitConfig inits the configuration, and fails the program if an error occurs
func InitConfig(configPath string) *config.AppConfig {
conf, err := config.New(configPath)
if err != nil {
Failf("Failed to load the configuration: %s", err.Error())
}
return conf
}
// Initstorage inits the storage, and fails the program if an error occurs
func InitStorage(config *config.AppConfig) storage.Storage {
s, err := services.InitStorage(config)
if err != nil {
Failf("Failed to init the storage: %s", err.Error())
}
return s
}

View file

@ -66,6 +66,7 @@ const (
defaultStorageSSLCaFile = "" defaultStorageSSLCaFile = ""
) )
// Deprecated: remove when we finally drop the JSON config
type BackendConfig struct { type BackendConfig struct {
Config *oidc.Config `json:"config"` Config *oidc.Config `json:"config"`
Name string `json:"name"` Name string `json:"name"`
@ -145,6 +146,10 @@ func (ac *AppConfig) getConfFromEnv() {
ac.StorageConfig.Ssl.Mode = getStringFromEnv(varStorageSSLMode, defaultStorageSSLMode) ac.StorageConfig.Ssl.Mode = getStringFromEnv(varStorageSSLMode, defaultStorageSSLMode)
} }
func (ac *AppConfig) RedirectURI() string {
return ac.OpenConnectConfig.Issuer + "/callback"
}
func New(filepath string) (*AppConfig, error) { func New(filepath string) (*AppConfig, error) {
var conf AppConfig var conf AppConfig
conf.StorageConfig = &StorageConfig{} conf.StorageConfig = &StorageConfig{}

View file

@ -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) {

View file

@ -1,6 +1,10 @@
package main package main
import "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd" import (
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/backend"
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/serve"
)
func main() { func main() {
cmd.Execute() cmd.Execute()

View 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}
}

View 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)
})
}

View file

@ -0,0 +1,34 @@
package services
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
// size in bytes of the client ids and secrets
const idSecretSize = 32
func generateRandomHex(size int) (string, error) {
raw := make([]byte, size)
n, err := rand.Read(raw)
if err != nil {
return "", fmt.Errorf("failed to read from random generator: %w", err)
}
if n != size {
return "", fmt.Errorf("failed to read from random generator (%d/%d)", n, size)
}
return hex.EncodeToString(raw), nil
}
func GenerateClientIDSecret() (string, string, error) {
clientID, err := generateRandomHex(idSecretSize)
if err != nil {
return "", "", fmt.Errorf("failed to generate client id: %w", err)
}
clientSecret, err := generateRandomHex(idSecretSize)
if err != nil {
return "", "", fmt.Errorf("failed to generate client secret: %w", err)
}
return clientID, clientSecret, nil
}

View file

@ -1,9 +1,11 @@
package services package services
import ( import (
"errors"
"fmt" "fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger"
"github.com/dexidp/dex/storage" "github.com/dexidp/dex/storage"
"github.com/dexidp/dex/storage/memory" "github.com/dexidp/dex/storage/memory"
@ -29,3 +31,10 @@ func InitStorage(conf *config.AppConfig) (storage.Storage, error) {
} }
return storageType, nil return storageType, nil
} }
func AddDefaultBackend(s storage.Storage) error {
if err := s.CreateConnector(connector.RefuseAllConnectorConfig); err != nil && !errors.Is(err, storage.ErrAlreadyExists) {
return fmt.Errorf("failed to add default backend: %w", err)
}
return nil
}