From c91f5f03be91e0e33988bbde3b56e20beb6e0a1f Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Wed, 8 Nov 2023 19:31:34 +0100 Subject: [PATCH] feat #44: add CLI commands to manage apps --- polyculeconnect/cmd/app/add.go | 101 +++++++++++++++++++++++ polyculeconnect/cmd/app/app.go | 28 +++++++ polyculeconnect/cmd/app/remove.go | 38 +++++++++ polyculeconnect/cmd/app/show.go | 68 +++++++++++++++ polyculeconnect/config/config.go | 1 + polyculeconnect/main.go | 1 + polyculeconnect/services/app/app.go | 34 ++++++++ polyculeconnect/services/app/app_test.go | 1 + polyculeconnect/services/idsecret.go | 8 +- 9 files changed, 276 insertions(+), 4 deletions(-) create mode 100644 polyculeconnect/cmd/app/add.go create mode 100644 polyculeconnect/cmd/app/app.go create mode 100644 polyculeconnect/cmd/app/remove.go create mode 100644 polyculeconnect/cmd/app/show.go create mode 100644 polyculeconnect/services/app/app.go create mode 100644 polyculeconnect/services/app/app_test.go diff --git a/polyculeconnect/cmd/app/add.go b/polyculeconnect/cmd/app/add.go new file mode 100644 index 0000000..f5bb62c --- /dev/null +++ b/polyculeconnect/cmd/app/add.go @@ -0,0 +1,101 @@ +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/app" + "github.com/dexidp/dex/storage" + "github.com/spf13/cobra" +) + +var ( + appID string + appClientID string + appClientSecret string + appName string + appRedirectURIs []string + appInteractive bool +) + +var appAddCmd = &cobra.Command{ + Use: "add", + Short: "Add a new app to the storage", + Long: `Add a new app to the storage. + +Parameters to provide: +- id: Unique ID to represent the app in the storage +- name: Human readable name to represent the app. +- redirect-uri: list of allowed redirection URIs for this app + +Optional parameters: +- client-id: Client ID used by the OpenIDConnect protocol, automatically generated if not provided +- client-secret: Client secret used by the OpenIDConnect protocol, automatically generated if not provided +- interactive: Pass this parameter to use a prompt to pass unset parameters (client id and secret)`, + Run: func(cmd *cobra.Command, args []string) { + addNewApp() + }, +} + +func generateSecret(interactive bool, currentValue, valueName string) (string, error) { + if currentValue != "" { + return currentValue, nil + } + if !interactive { + val, err := services.GenerateRandomHex(services.IDSecretSize) + if err != nil { + return "", fmt.Errorf("failed to generate %s: %w", valueName, err) + } + return val, nil + } + fmt.Printf("Enter value for %s, use an empty value to automatically generate it.\n", valueName) + var enteredVal string + fmt.Scanln(&enteredVal) + if enteredVal == "" { + return generateSecret(false, currentValue, valueName) + } + return enteredVal, nil +} + +func addNewApp() { + c := utils.InitConfig("") + s := utils.InitStorage(c) + + clientID, err := generateSecret(appInteractive, appClientID, "client ID") + if err != nil { + utils.Fail(err.Error()) + } + clientSecret, err := generateSecret(appInteractive, appClientSecret, "client secret") + if err != nil { + utils.Fail(err.Error()) + } + + appConf := storage.Client{ + ID: clientID, + Secret: clientSecret, + Name: appName, + RedirectURIs: appRedirectURIs, + } + if err := app.New(s).AddApp(appConf); err != nil { + utils.Failf("Failed to add new app to storage: %s", err.Error()) + } + + fmt.Printf("New app %s added.\n", appName) + printProperty("Client ID", clientID, 1) + printProperty("Client secret", clientSecret, 1) +} + +func init() { + appCmd.AddCommand(appAddCmd) + + appAddCmd.Flags().StringVarP(&appName, "name", "n", "", "Name to represent the app") + appAddCmd.Flags().StringVarP(&appClientID, "id", "i", "", "ID to identify the app in the storage") + appAddCmd.Flags().StringVarP(&appClientSecret, "secret", "s", "", "OpenIDConnect client secret") + appAddCmd.Flags().StringSliceVarP(&appRedirectURIs, "redirect-uri", "r", []string{}, "Allowed redirect URI") + + appAddCmd.Flags().BoolVar(&appInteractive, "interactive", false, "Set the client ID and secret in an interactive way") + + appAddCmd.MarkFlagRequired("name") + appAddCmd.MarkFlagRequired("redirect-uri") +} diff --git a/polyculeconnect/cmd/app/app.go b/polyculeconnect/cmd/app/app.go new file mode 100644 index 0000000..9697395 --- /dev/null +++ b/polyculeconnect/cmd/app/app.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "fmt" + "strings" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd" + "github.com/spf13/cobra" +) + +var appCmd = &cobra.Command{ + Use: "app", + Short: "Handle client applications", + Long: `Add, Remove or Show currently installed client applications`, +} + +func printProperty(key, value string, indent int) { + prefix := strings.Repeat("\t", indent) + keyStr := "" + if key != "" { + keyStr = key + ": " + } + fmt.Printf("%s- %s%s\n", prefix, keyStr, value) +} + +func init() { + cmd.RootCmd.AddCommand(appCmd) +} diff --git a/polyculeconnect/cmd/app/remove.go b/polyculeconnect/cmd/app/remove.go new file mode 100644 index 0000000..252f4be --- /dev/null +++ b/polyculeconnect/cmd/app/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/app" + "github.com/dexidp/dex/storage" + "github.com/spf13/cobra" +) + +var appRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove an app", + Long: `Remove the app with the given ID from the database. + +If the app is not found in the database, no error is returned`, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + removeApp(args[0]) + }, +} + +func removeApp(appID string) { + s := utils.InitStorage(utils.InitConfig("")) + + if err := app.New(s).RemoveApp(appID); err != nil { + if !errors.Is(err, storage.ErrNotFound) { + utils.Failf("Failed to remove app: %s", err.Error()) + } + } + fmt.Println("App deleted") +} + +func init() { + appCmd.AddCommand(appRemoveCmd) +} diff --git a/polyculeconnect/cmd/app/show.go b/polyculeconnect/cmd/app/show.go new file mode 100644 index 0000000..4b58984 --- /dev/null +++ b/polyculeconnect/cmd/app/show.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "errors" + "fmt" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/utils" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/app" + "github.com/dexidp/dex/storage" + "github.com/spf13/cobra" +) + +var appShowCmd = &cobra.Command{ + Use: "show [app_id]", + Short: "Display installed apps", + Long: `Display the configuration for the apps. + +Pass the commands without arguments to display the list of currently installed apps +Pass the optional 'id' argument to display the configuration for this specific app`, + Args: cobra.MaximumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + s := utils.InitStorage(utils.InitConfig("")) + + if len(args) > 0 { + showApp(args[0], app.New(s)) + } else { + listApps(app.New(s)) + } + }, +} + +func showApp(appID string, appService app.Service) { + appConfig, err := appService.GetApp(appID) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + utils.Failf("App with ID %s does not exist\n", appID) + } + utils.Failf("Failed to get config for app %s: %q\n", appID, err.Error()) + } + + fmt.Println("App config:") + printProperty("Name", appConfig.Name, 1) + printProperty("ID", appConfig.ID, 1) + printProperty("Client secret", appConfig.Secret, 1) + printProperty("Redirect URIs", "", 1) + for _, uri := range appConfig.RedirectURIs { + printProperty("", uri, 2) + } +} + +func listApps(appService app.Service) { + apps, err := appService.ListApps() + if err != nil { + utils.Failf("Failed to list apps: %q\n", err.Error()) + } + + if len(apps) == 0 { + fmt.Println("No app configured") + return + } + for _, b := range apps { + fmt.Printf("\t - %s: (%s)\n", b.ID, b.Name) + } +} + +func init() { + appCmd.AddCommand(appShowCmd) +} diff --git a/polyculeconnect/config/config.go b/polyculeconnect/config/config.go index c833165..690d023 100644 --- a/polyculeconnect/config/config.go +++ b/polyculeconnect/config/config.go @@ -75,6 +75,7 @@ type BackendConfig struct { Local bool `json:"local"` } +// Deprecated: remove when we finally drop the JSON config type OpenConnectConfig struct { ClientConfigs []*storage.Client `json:"clients"` BackendConfigs []*BackendConfig `json:"backends"` diff --git a/polyculeconnect/main.go b/polyculeconnect/main.go index 51395ff..2cf4d4d 100644 --- a/polyculeconnect/main.go +++ b/polyculeconnect/main.go @@ -2,6 +2,7 @@ package main import ( "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd" + _ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/app" _ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/backend" _ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/serve" ) diff --git a/polyculeconnect/services/app/app.go b/polyculeconnect/services/app/app.go new file mode 100644 index 0000000..5d9095d --- /dev/null +++ b/polyculeconnect/services/app/app.go @@ -0,0 +1,34 @@ +package app + +import "github.com/dexidp/dex/storage" + +type Service interface { + ListApps() ([]storage.Client, error) + GetApp(id string) (storage.Client, error) + AddApp(config storage.Client) error + RemoveApp(id string) error +} + +type concreteAppService struct { + s storage.Storage +} + +func (cas *concreteAppService) ListApps() ([]storage.Client, error) { + return cas.s.ListClients() +} + +func (cas *concreteAppService) GetApp(id string) (storage.Client, error) { + return cas.s.GetClient(id) +} + +func (cas *concreteAppService) AddApp(config storage.Client) error { + return cas.s.CreateClient(config) +} + +func (cas *concreteAppService) RemoveApp(id string) error { + return cas.s.DeleteClient(id) +} + +func New(s storage.Storage) Service { + return &concreteAppService{s} +} diff --git a/polyculeconnect/services/app/app_test.go b/polyculeconnect/services/app/app_test.go new file mode 100644 index 0000000..9e88d52 --- /dev/null +++ b/polyculeconnect/services/app/app_test.go @@ -0,0 +1 @@ +package app_test diff --git a/polyculeconnect/services/idsecret.go b/polyculeconnect/services/idsecret.go index 373a24f..dea2ce8 100644 --- a/polyculeconnect/services/idsecret.go +++ b/polyculeconnect/services/idsecret.go @@ -7,9 +7,9 @@ import ( ) // size in bytes of the client ids and secrets -const idSecretSize = 32 +const IDSecretSize = 32 -func generateRandomHex(size int) (string, error) { +func GenerateRandomHex(size int) (string, error) { raw := make([]byte, size) n, err := rand.Read(raw) if err != nil { @@ -22,11 +22,11 @@ func generateRandomHex(size int) (string, error) { } func GenerateClientIDSecret() (string, string, error) { - clientID, err := generateRandomHex(idSecretSize) + clientID, err := GenerateRandomHex(IDSecretSize) if err != nil { return "", "", fmt.Errorf("failed to generate client id: %w", err) } - clientSecret, err := generateRandomHex(idSecretSize) + clientSecret, err := GenerateRandomHex(IDSecretSize) if err != nil { return "", "", fmt.Errorf("failed to generate client secret: %w", err) }