Feat #44: Add CLI command for the apps #14

Merged
faercol merged 1 commit from 44-add-cli-clients-commands into main 2023-12-13 17:33:58 +00:00
9 changed files with 276 additions and 4 deletions

View file

@ -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")
}

View file

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

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/app"
"github.com/dexidp/dex/storage"
"github.com/spf13/cobra"
)
var appRemoveCmd = &cobra.Command{
Use: "remove <app_client_id>",
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)
}

View file

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

View file

@ -75,6 +75,7 @@ type BackendConfig struct {
Local bool `json:"local"` Local bool `json:"local"`
} }
// Deprecated: remove when we finally drop the JSON config
type OpenConnectConfig struct { type OpenConnectConfig struct {
ClientConfigs []*storage.Client `json:"clients"` ClientConfigs []*storage.Client `json:"clients"`
BackendConfigs []*BackendConfig `json:"backends"` BackendConfigs []*BackendConfig `json:"backends"`

View file

@ -2,6 +2,7 @@ package main
import ( import (
"git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd" "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/backend"
_ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/serve" _ "git.faercol.me/faercol/polyculeconnect/polyculeconnect/cmd/serve"
) )

View file

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

View file

@ -0,0 +1 @@
package app_test

View file

@ -7,9 +7,9 @@ import (
) )
// size in bytes of the client ids and secrets // 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) raw := make([]byte, size)
n, err := rand.Read(raw) n, err := rand.Read(raw)
if err != nil { if err != nil {
@ -22,11 +22,11 @@ func generateRandomHex(size int) (string, error) {
} }
func GenerateClientIDSecret() (string, string, error) { func GenerateClientIDSecret() (string, string, error) {
clientID, err := generateRandomHex(idSecretSize) clientID, err := GenerateRandomHex(IDSecretSize)
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to generate client id: %w", err) return "", "", fmt.Errorf("failed to generate client id: %w", err)
} }
clientSecret, err := generateRandomHex(idSecretSize) clientSecret, err := GenerateRandomHex(IDSecretSize)
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to generate client secret: %w", err) return "", "", fmt.Errorf("failed to generate client secret: %w", err)
} }