From 69a07ce076e786b566fc0e693bf935a2b245e8e9 Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sun, 29 Oct 2023 13:31:52 +0100 Subject: [PATCH] feat #43: add cli command to manage backends --- polyculeconnect/cmd/backend/add.go | 66 +++++++++++++++++++++++++ polyculeconnect/cmd/backend/backend.go | 30 ++++++++++++ polyculeconnect/cmd/backend/remove.go | 38 +++++++++++++++ polyculeconnect/cmd/backend/show.go | 67 ++++++++++++++++++++++++++ polyculeconnect/cmd/root.go | 8 +-- polyculeconnect/cmd/utils/utils.go | 40 +++++++++++++++ polyculeconnect/config/config.go | 5 ++ polyculeconnect/main.go | 5 +- polyculeconnect/services/idsecret.go | 34 +++++++++++++ 9 files changed, 288 insertions(+), 5 deletions(-) create mode 100644 polyculeconnect/cmd/backend/add.go create mode 100644 polyculeconnect/cmd/backend/backend.go create mode 100644 polyculeconnect/cmd/backend/remove.go create mode 100644 polyculeconnect/cmd/backend/show.go create mode 100644 polyculeconnect/cmd/utils/utils.go create mode 100644 polyculeconnect/services/idsecret.go diff --git a/polyculeconnect/cmd/backend/add.go b/polyculeconnect/cmd/backend/add.go new file mode 100644 index 0000000..7577736 --- /dev/null +++ b/polyculeconnect/cmd/backend/add.go @@ -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") +} diff --git a/polyculeconnect/cmd/backend/backend.go b/polyculeconnect/cmd/backend/backend.go new file mode 100644 index 0000000..a45fb6c --- /dev/null +++ b/polyculeconnect/cmd/backend/backend.go @@ -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) +} diff --git a/polyculeconnect/cmd/backend/remove.go b/polyculeconnect/cmd/backend/remove.go new file mode 100644 index 0000000..de6fe0e --- /dev/null +++ b/polyculeconnect/cmd/backend/remove.go @@ -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 ", + 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) +} diff --git a/polyculeconnect/cmd/backend/show.go b/polyculeconnect/cmd/backend/show.go new file mode 100644 index 0000000..e1a6ce6 --- /dev/null +++ b/polyculeconnect/cmd/backend/show.go @@ -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) +} diff --git a/polyculeconnect/cmd/root.go b/polyculeconnect/cmd/root.go index 31e70d5..3e792a3 100644 --- a/polyculeconnect/cmd/root.go +++ b/polyculeconnect/cmd/root.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" ) -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ Use: "polyculeconnect", Short: "You're in their DMs, I'm in their SSO", 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. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { - err := rootCmd.Execute() + err := RootCmd.Execute() if err != nil { os.Exit(1) } @@ -38,5 +38,5 @@ func init() { // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // Disable the default `completion` command to generate the autocompletion files - rootCmd.Root().CompletionOptions.DisableDefaultCmd = true + RootCmd.Root().CompletionOptions.DisableDefaultCmd = true } diff --git a/polyculeconnect/cmd/utils/utils.go b/polyculeconnect/cmd/utils/utils.go new file mode 100644 index 0000000..137d768 --- /dev/null +++ b/polyculeconnect/cmd/utils/utils.go @@ -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 +} diff --git a/polyculeconnect/config/config.go b/polyculeconnect/config/config.go index 8fdb135..c833165 100644 --- a/polyculeconnect/config/config.go +++ b/polyculeconnect/config/config.go @@ -66,6 +66,7 @@ const ( defaultStorageSSLCaFile = "" ) +// Deprecated: remove when we finally drop the JSON config type BackendConfig struct { Config *oidc.Config `json:"config"` Name string `json:"name"` @@ -145,6 +146,10 @@ func (ac *AppConfig) getConfFromEnv() { ac.StorageConfig.Ssl.Mode = getStringFromEnv(varStorageSSLMode, defaultStorageSSLMode) } +func (ac *AppConfig) RedirectURI() string { + return ac.OpenConnectConfig.Issuer + "/callback" +} + func New(filepath string) (*AppConfig, error) { var conf AppConfig conf.StorageConfig = &StorageConfig{} diff --git a/polyculeconnect/main.go b/polyculeconnect/main.go index 51129fe..d7b8e78 100644 --- a/polyculeconnect/main.go +++ b/polyculeconnect/main.go @@ -1,6 +1,9 @@ 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" +) func main() { cmd.Execute() diff --git a/polyculeconnect/services/idsecret.go b/polyculeconnect/services/idsecret.go new file mode 100644 index 0000000..373a24f --- /dev/null +++ b/polyculeconnect/services/idsecret.go @@ -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 +}