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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
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 = ""
|
||||
)
|
||||
|
||||
// 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{}
|
||||
|
|
|
@ -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()
|
||||
|
|
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