feat #43: add cli command to manage backends

This commit is contained in:
Melora Hugues 2023-10-29 13:31:52 +01:00
parent 550622e512
commit 69a07ce076
9 changed files with 288 additions and 5 deletions

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

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

@ -1,6 +1,9 @@
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"
)
func main() { func main() {
cmd.Execute() cmd.Execute()

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
}