Feat #43: Add CLI command for the backends #11
9 changed files with 288 additions and 5 deletions
66
polyculeconnect/cmd/backend/add.go
Normal file
66
polyculeconnect/cmd/backend/add.go
Normal 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")
|
||||||
|
}
|
30
polyculeconnect/cmd/backend/backend.go
Normal file
30
polyculeconnect/cmd/backend/backend.go
Normal 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)
|
||||||
|
}
|
38
polyculeconnect/cmd/backend/remove.go
Normal file
38
polyculeconnect/cmd/backend/remove.go
Normal 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)
|
||||||
|
}
|
67
polyculeconnect/cmd/backend/show.go
Normal file
67
polyculeconnect/cmd/backend/show.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
40
polyculeconnect/cmd/utils/utils.go
Normal file
40
polyculeconnect/cmd/utils/utils.go
Normal 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
|
||||||
|
}
|
|
@ -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{}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
34
polyculeconnect/services/idsecret.go
Normal file
34
polyculeconnect/services/idsecret.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue