Feat #44: Add CLI command for the apps #14
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"`
|
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"`
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
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
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue