feat #44: add CLI commands to manage apps
This commit is contained in:
parent
9d2d49425d
commit
c8958a8f44
9 changed files with 276 additions and 4 deletions
101
polyculeconnect/cmd/app/add.go
Normal file
101
polyculeconnect/cmd/app/add.go
Normal 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")
|
||||
}
|
28
polyculeconnect/cmd/app/app.go
Normal file
28
polyculeconnect/cmd/app/app.go
Normal 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)
|
||||
}
|
38
polyculeconnect/cmd/app/remove.go
Normal file
38
polyculeconnect/cmd/app/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/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)
|
||||
}
|
68
polyculeconnect/cmd/app/show.go
Normal file
68
polyculeconnect/cmd/app/show.go
Normal 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)
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
34
polyculeconnect/services/app/app.go
Normal file
34
polyculeconnect/services/app/app.go
Normal 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}
|
||||
}
|
1
polyculeconnect/services/app/app_test.go
Normal file
1
polyculeconnect/services/app/app_test.go
Normal file
|
@ -0,0 +1 @@
|
|||
package app_test
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue