Compare commits

..

4 commits

Author SHA1 Message Date
fa20b9583b chore: refactor serve command
All checks were successful
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/deploy Pipeline was successful
2023-10-29 13:32:09 +01:00
5753630553 feat #43: add cli command to manage backends 2023-10-29 13:31:52 +01:00
930e94f820 feat #43: add service to handle backends in the storage 2023-10-29 13:29:48 +01:00
897b9273fc Add envrc to facilitate development 2023-10-29 13:27:13 +01:00
18 changed files with 529 additions and 240 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

@ -1,11 +1,11 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd package cmd
import ( import (
"fmt" "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" "github.com/spf13/cobra"
) )
@ -15,7 +15,6 @@ var (
backendIssuer string backendIssuer string
) )
// backendAddCmd represents the add command
var backendAddCmd = &cobra.Command{ var backendAddCmd = &cobra.Command{
Use: "add", Use: "add",
Short: "Add a new backend to the storage", Short: "Add a new backend to the storage",
@ -28,22 +27,39 @@ Parameters to provide:
authentication authentication
- issuer: Full hostname of the OIDC provider, e.g. 'https://github.com'`, - issuer: Full hostname of the OIDC provider, e.g. 'https://github.com'`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("add called") 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() { func init() {
backendCmd.AddCommand(backendAddCmd) backendCmd.AddCommand(backendAddCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// backendAddCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// backendAddCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
backendAddCmd.Flags().StringVarP(&backendID, "id", "i", "", "ID to identify the backend in the storage") 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(&backendName, "name", "n", "", "Name to represent the backend")
backendAddCmd.Flags().StringVarP(&backendIssuer, "issuer", "d", "", "Full hostname of the backend") backendAddCmd.Flags().StringVarP(&backendIssuer, "issuer", "d", "", "Full hostname of the backend")

View file

@ -1,16 +1,12 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd package cmd
import ( import (
"fmt" "fmt"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// backendCmd represents the backend command
var backendCmd = &cobra.Command{ var backendCmd = &cobra.Command{
Use: "backend", Use: "backend",
Short: "A brief description of your command", Short: "A brief description of your command",
@ -25,16 +21,10 @@ to quickly create a Cobra application.`,
}, },
} }
func init() { func printProperty(key, value string) {
rootCmd.AddCommand(backendCmd) fmt.Printf("\t- %s: %s\n", key, value)
}
// Here you will define your flags and configuration settings.
func init() {
// Cobra supports Persistent Flags which will work for this command cmd.RootCmd.AddCommand(backendCmd)
// and all subcommands, e.g.:
// backendCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// backendCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }

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

@ -1,39 +0,0 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// backendRemoveCmd represents the remove command
var backendRemoveCmd = &cobra.Command{
Use: "remove",
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("remove called")
},
}
func init() {
backendCmd.AddCommand(backendRemoveCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// backendRemoveCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// backendRemoveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View file

@ -1,97 +0,0 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"errors"
"fmt"
"os"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/config"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services"
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend"
"github.com/dexidp/dex/storage"
"github.com/spf13/cobra"
)
// backendShowCmd represents the show command for backends
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) {
conf, err := config.New(configPath)
if err != nil {
panic(err)
}
s, err := services.InitStorage(conf)
if err != nil {
panic(err)
}
if len(args) > 0 {
showBackend(args[0], backend.New(s))
} else {
listBackends(backend.New(s))
}
},
}
func printProperty(key, value string) {
fmt.Printf("\t- %s: %s\n", key, value)
}
func showBackend(backendId string, backendService backend.Service) {
backendConfig, err := backendService.GetBackend(backendId)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
fmt.Fprintf(os.Stderr, "Backend with ID %s does not exist\n", backendId)
} else {
fmt.Fprintf(os.Stderr, "Failed to get config for backend %s: %q\n", backendId, err.Error())
}
return
}
fmt.Println("Backend config:")
printProperty("ID", backendConfig.Storage.ID)
printProperty("Name", backendConfig.Storage.Name)
printProperty("Issuer", backendConfig.OIDC.Issuer)
printProperty("Client ID", backendConfig.OIDC.ClientID)
printProperty("Client secret", backendConfig.OIDC.ClientSecret)
}
func listBackends(backendService backend.Service) {
backends, err := backendService.ListBackends()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list backends: %q\n", err.Error())
return
}
if len(backends) == 0 {
fmt.Println("No backend configured")
} else {
for _, b := range backends {
fmt.Printf("\t - backend %s: id %s issuer %s\n", b.Storage.Name, b.Storage.ID, b.OIDC.Issuer)
}
}
}
func init() {
backendCmd.AddCommand(backendShowCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// backendShowCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// backendShowCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

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 DM, I'm in their SSO", Short: "You're in their DM, I'm in their SSO",
Long: `PolyculeConnect is a SSO OpenIDConnect provider allowing multiple authentication Long: `PolyculeConnect is a SSO OpenIDConnect provider allowing multiple authentication
@ -20,7 +20,7 @@ backends, and enabling 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{
@ -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,10 +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" 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

@ -13,18 +13,51 @@ import (
var ErrUnsupportedType = errors.New("unsupported connector type") var ErrUnsupportedType = errors.New("unsupported connector type")
type BackendConfig struct { type BackendConfig struct {
OIDC oidc.Config ID string
Storage storage.Connector 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 { func (bc *BackendConfig) FromConnector(connector storage.Connector) error {
var oidc oidc.Config
if connector.Type != "oidc" { if connector.Type != "oidc" {
return ErrUnsupportedType return ErrUnsupportedType
} }
if err := json.Unmarshal(connector.Config, &bc.OIDC); err != nil { if err := json.Unmarshal(connector.Config, &oidc); err != nil {
return fmt.Errorf("invalid OIDC config: %w", err) return fmt.Errorf("invalid OIDC config: %w", err)
} }
bc.Storage = connector 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 return nil
} }
@ -74,7 +107,11 @@ func (cbs *concreteBackendService) GetBackend(connectorID string) (BackendConfig
} }
func (cbs *concreteBackendService) AddBackend(config BackendConfig) error { func (cbs *concreteBackendService) AddBackend(config BackendConfig) error {
return cbs.s.CreateConnector(config.Storage) 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 { func (cbs *concreteBackendService) RemoveBackend(connectorID string) error {

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

@ -1,42 +0,0 @@
package test
import "github.com/dexidp/dex/storage"
type MockBackendService struct {
Connectors map[string]storage.Connector
ListError error
GetError error
AddError error
RemoveError error
}
func (mbs *MockBackendService) ListConnectors() ([]storage.Connector, error) {
var res []storage.Connector
for _, c := range mbs.Connectors {
res = append(res, c)
}
return res, mbs.ListError
}
func (mbs *MockBackendService) GetConnector(connectorID string) (storage.Connector, error) {
if res, ok := mbs.Connectors[connectorID]; ok {
return res, mbs.GetError
}
return storage.Connector{}, storage.ErrNotFound
}
func (mbs *MockBackendService) AddConnector(config storage.Connector) error {
if _, ok := mbs.Connectors[config.ID]; ok {
return storage.ErrAlreadyExists
}
if mbs.AddError != nil {
return mbs.AddError
}
mbs.Connectors[config.ID] = config
return nil
}
func (mbs *MockBackendService) RemoveConnector(connectorID string) error {
delete(mbs.Connectors, connectorID)
return mbs.RemoveError
}

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
}