From 74cb316d78c0435750489dbf8ab3b8fc63e8f96c Mon Sep 17 00:00:00 2001 From: Melora Hugues Date: Sat, 28 Oct 2023 16:51:00 +0200 Subject: [PATCH] feat #43: add CLI commands to manage backends --- polyculeconnect/cmd/backend.go | 40 ++++++++ polyculeconnect/cmd/backend_add.go | 50 ++++++++++ polyculeconnect/cmd/backend_remove.go | 39 ++++++++ polyculeconnect/cmd/backend_show.go | 97 +++++++++++++++++++ polyculeconnect/cmd/serve.go | 2 +- polyculeconnect/connector/refuse_all.go | 2 + polyculeconnect/services/backend/backend.go | 86 ++++++++++++++++ polyculeconnect/services/backend/test/mock.go | 42 ++++++++ 8 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 polyculeconnect/cmd/backend.go create mode 100644 polyculeconnect/cmd/backend_add.go create mode 100644 polyculeconnect/cmd/backend_remove.go create mode 100644 polyculeconnect/cmd/backend_show.go create mode 100644 polyculeconnect/services/backend/backend.go create mode 100644 polyculeconnect/services/backend/test/mock.go diff --git a/polyculeconnect/cmd/backend.go b/polyculeconnect/cmd/backend.go new file mode 100644 index 0000000..d1ab467 --- /dev/null +++ b/polyculeconnect/cmd/backend.go @@ -0,0 +1,40 @@ +/* +Copyright © 2023 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// backendCmd represents the backend command +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 init() { + rootCmd.AddCommand(backendCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backendCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backendCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/polyculeconnect/cmd/backend_add.go b/polyculeconnect/cmd/backend_add.go new file mode 100644 index 0000000..84a1bb1 --- /dev/null +++ b/polyculeconnect/cmd/backend_add.go @@ -0,0 +1,50 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var ( + backendID string + backendName string + backendIssuer string +) + +// backendAddCmd represents the add command +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) { + fmt.Println("add called") + }, +} + +func init() { + backendCmd.AddCommand(backendAddCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backendAddCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backendAddCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + 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") +} diff --git a/polyculeconnect/cmd/backend_remove.go b/polyculeconnect/cmd/backend_remove.go new file mode 100644 index 0000000..cce30e8 --- /dev/null +++ b/polyculeconnect/cmd/backend_remove.go @@ -0,0 +1,39 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// backendRemoveCmd represents the remove command +var backendRemoveCmd = &cobra.Command{ + Use: "remove", + 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("remove called") + }, +} + +func init() { + backendCmd.AddCommand(backendRemoveCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backendRemoveCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backendRemoveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/polyculeconnect/cmd/backend_show.go b/polyculeconnect/cmd/backend_show.go new file mode 100644 index 0000000..aa6d85d --- /dev/null +++ b/polyculeconnect/cmd/backend_show.go @@ -0,0 +1,97 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "errors" + "fmt" + "os" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/backend" + "github.com/dexidp/dex/storage" + "github.com/spf13/cobra" +) + +// backendShowCmd represents the show command for backends +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) { + conf, err := config.New(configPath) + if err != nil { + panic(err) + } + + s, err := services.InitStorage(conf) + if err != nil { + panic(err) + } + + if len(args) > 0 { + showBackend(args[0], backend.New(s)) + } else { + listBackends(backend.New(s)) + } + }, +} + +func printProperty(key, value string) { + fmt.Printf("\t- %s: %s\n", key, value) +} + +func showBackend(backendId string, backendService backend.Service) { + backendConfig, err := backendService.GetBackend(backendId) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + fmt.Fprintf(os.Stderr, "Backend with ID %s does not exist\n", backendId) + } else { + fmt.Fprintf(os.Stderr, "Failed to get config for backend %s: %q\n", backendId, err.Error()) + } + return + } + + fmt.Println("Backend config:") + printProperty("ID", backendConfig.Storage.ID) + printProperty("Name", backendConfig.Storage.Name) + printProperty("Issuer", backendConfig.OIDC.Issuer) + printProperty("Client ID", backendConfig.OIDC.ClientID) + printProperty("Client secret", backendConfig.OIDC.ClientSecret) +} + +func listBackends(backendService backend.Service) { + backends, err := backendService.ListBackends() + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to list backends: %q\n", err.Error()) + return + } + + if len(backends) == 0 { + fmt.Println("No backend configured") + } else { + for _, b := range backends { + fmt.Printf("\t - backend %s: id %s issuer %s\n", b.Storage.Name, b.Storage.ID, b.OIDC.Issuer) + } + } +} + +func init() { + backendCmd.AddCommand(backendShowCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backendShowCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backendShowCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/polyculeconnect/cmd/serve.go b/polyculeconnect/cmd/serve.go index c12c7fe..e5c8682 100644 --- a/polyculeconnect/cmd/serve.go +++ b/polyculeconnect/cmd/serve.go @@ -64,7 +64,7 @@ func serve() { logger.L.Info("Initializing authentication backends") - dex_server.ConnectorsConfig["refuseAll"] = func() dex_server.ConnectorConfig { return new(connector.RefuseAllConfig) } + dex_server.ConnectorsConfig[connector.TypeRefuseAll] = func() dex_server.ConnectorConfig { return new(connector.RefuseAllConfig) } connectors, err := dexConf.Storage.ListConnectors() if err != nil { logger.L.Fatalf("Failed to get existing connectors: %s", err.Error()) diff --git a/polyculeconnect/connector/refuse_all.go b/polyculeconnect/connector/refuse_all.go index edb862c..92f131b 100644 --- a/polyculeconnect/connector/refuse_all.go +++ b/polyculeconnect/connector/refuse_all.go @@ -8,6 +8,8 @@ import ( "github.com/dexidp/dex/pkg/log" ) +const TypeRefuseAll = "refuseAll" + type RefuseAllConfig struct{} func (c *RefuseAllConfig) Open(id string, logger log.Logger) (connector.Connector, error) { diff --git a/polyculeconnect/services/backend/backend.go b/polyculeconnect/services/backend/backend.go new file mode 100644 index 0000000..f91903a --- /dev/null +++ b/polyculeconnect/services/backend/backend.go @@ -0,0 +1,86 @@ +package backend + +import ( + "encoding/json" + "errors" + "fmt" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/connector" + "github.com/dexidp/dex/connector/oidc" + "github.com/dexidp/dex/storage" +) + +var ErrUnsupportedType = errors.New("unsupported connector type") + +type BackendConfig struct { + OIDC oidc.Config + Storage storage.Connector +} + +func (bc *BackendConfig) FromConnector(connector storage.Connector) error { + if connector.Type != "oidc" { + return ErrUnsupportedType + } + if err := json.Unmarshal(connector.Config, &bc.OIDC); err != nil { + return fmt.Errorf("invalid OIDC config: %w", err) + } + bc.Storage = connector + return nil +} + +type Service interface { + ListBackends() ([]BackendConfig, error) + GetBackend(id string) (BackendConfig, error) + AddBackend(config BackendConfig) error + RemoveBackend(id string) error +} + +type concreteBackendService struct { + s storage.Storage +} + +func (cbs *concreteBackendService) ListBackends() ([]BackendConfig, error) { + connectors, err := cbs.s.ListConnectors() + if err != nil { + return nil, fmt.Errorf("failed to get connectors from storage: %w", err) + } + var res []BackendConfig + for _, c := range connectors { + + // We know that this type is special, we don't want to use it at all here + if c.Type == connector.TypeRefuseAll { + continue + } + + var b BackendConfig + if err := b.FromConnector(c); err != nil { + return res, err + } + res = append(res, b) + } + return res, nil +} + +func (cbs *concreteBackendService) GetBackend(connectorID string) (BackendConfig, error) { + c, err := cbs.s.GetConnector(connectorID) + if err != nil { + return BackendConfig{}, fmt.Errorf("failed to get connector from storage: %w", err) + } + var res BackendConfig + if err := res.FromConnector(c); err != nil { + return BackendConfig{}, err + } + return res, nil +} + +func (cbs *concreteBackendService) AddBackend(config BackendConfig) error { + return cbs.s.CreateConnector(config.Storage) +} + +func (cbs *concreteBackendService) RemoveBackend(connectorID string) error { + return cbs.s.DeleteConnector(connectorID) +} + +func New(s storage.Storage) Service { + return &concreteBackendService{s} +} diff --git a/polyculeconnect/services/backend/test/mock.go b/polyculeconnect/services/backend/test/mock.go new file mode 100644 index 0000000..532d4ee --- /dev/null +++ b/polyculeconnect/services/backend/test/mock.go @@ -0,0 +1,42 @@ +package test + +import "github.com/dexidp/dex/storage" + +type MockBackendService struct { + Connectors map[string]storage.Connector + ListError error + GetError error + AddError error + RemoveError error +} + +func (mbs *MockBackendService) ListConnectors() ([]storage.Connector, error) { + var res []storage.Connector + for _, c := range mbs.Connectors { + res = append(res, c) + } + return res, mbs.ListError +} + +func (mbs *MockBackendService) GetConnector(connectorID string) (storage.Connector, error) { + if res, ok := mbs.Connectors[connectorID]; ok { + return res, mbs.GetError + } + return storage.Connector{}, storage.ErrNotFound +} + +func (mbs *MockBackendService) AddConnector(config storage.Connector) error { + if _, ok := mbs.Connectors[config.ID]; ok { + return storage.ErrAlreadyExists + } + if mbs.AddError != nil { + return mbs.AddError + } + mbs.Connectors[config.ID] = config + return nil +} + +func (mbs *MockBackendService) RemoveConnector(connectorID string) error { + delete(mbs.Connectors, connectorID) + return mbs.RemoveError +}