From 8c9c2518f56d2b5c5ec2c476b42ec5f28ea09875 Mon Sep 17 00:00:00 2001 From: rithu john Date: Mon, 17 Apr 2017 15:41:41 -0700 Subject: [PATCH] server: account for dynamically changing connector object in storage. --- cmd/dex/config.go | 62 ++++++------ cmd/dex/config_test.go | 2 +- cmd/dex/serve.go | 60 ++++++------ server/handlers.go | 51 ++++++---- server/server.go | 151 ++++++++++++++++++++++++----- server/server_test.go | 36 +++---- storage/conformance/conformance.go | 4 + storage/memory/static_test.go | 91 +++++++++++++++++ storage/static.go | 70 +++++++++++++ storage/storage.go | 10 +- 10 files changed, 400 insertions(+), 137 deletions(-) diff --git a/cmd/dex/config.go b/cmd/dex/config.go index 7279d29a..2c0cae29 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -9,13 +9,6 @@ import ( "github.com/Sirupsen/logrus" "golang.org/x/crypto/bcrypt" - "github.com/coreos/dex/connector" - "github.com/coreos/dex/connector/github" - "github.com/coreos/dex/connector/gitlab" - "github.com/coreos/dex/connector/ldap" - "github.com/coreos/dex/connector/mock" - "github.com/coreos/dex/connector/oidc" - "github.com/coreos/dex/connector/saml" "github.com/coreos/dex/server" "github.com/coreos/dex/storage" "github.com/coreos/dex/storage/kubernetes" @@ -25,17 +18,20 @@ import ( // Config is the config format for the main application. type Config struct { - Issuer string `json:"issuer"` - Storage Storage `json:"storage"` - Connectors []Connector `json:"connectors"` - Web Web `json:"web"` - OAuth2 OAuth2 `json:"oauth2"` - GRPC GRPC `json:"grpc"` - Expiry Expiry `json:"expiry"` - Logger Logger `json:"logger"` + Issuer string `json:"issuer"` + Storage Storage `json:"storage"` + Web Web `json:"web"` + OAuth2 OAuth2 `json:"oauth2"` + GRPC GRPC `json:"grpc"` + Expiry Expiry `json:"expiry"` + Logger Logger `json:"logger"` Frontend server.WebConfig `json:"frontend"` + // StaticConnectors are user defined connectors specified in the ConfigMap + // Write operations, like updating a connector, will fail. + StaticConnectors []Connector `json:"connectors"` + // StaticClients cause the server to use this list of clients rather than // querying the storage. Write operations, like creating a client, will fail. StaticClients []storage.Client `json:"staticClients"` @@ -170,24 +166,7 @@ type Connector struct { Name string `json:"name"` ID string `json:"id"` - Config ConnectorConfig `json:"config"` -} - -// ConnectorConfig is a configuration that can open a connector. -type ConnectorConfig interface { - Open(logrus.FieldLogger) (connector.Connector, error) -} - -var connectors = map[string]func() ConnectorConfig{ - "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, - "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, - "ldap": func() ConnectorConfig { return new(ldap.Config) }, - "github": func() ConnectorConfig { return new(github.Config) }, - "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, - "oidc": func() ConnectorConfig { return new(oidc.Config) }, - "saml": func() ConnectorConfig { return new(saml.Config) }, - // Keep around for backwards compatibility. - "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, + Config server.ConnectorConfig `json:"config"` } // UnmarshalJSON allows Connector to implement the unmarshaler interface to @@ -203,7 +182,7 @@ func (c *Connector) UnmarshalJSON(b []byte) error { if err := json.Unmarshal(b, &conn); err != nil { return fmt.Errorf("parse connector: %v", err) } - f, ok := connectors[conn.Type] + f, ok := server.ConnectorsConfig[conn.Type] if !ok { return fmt.Errorf("unknown connector type %q", conn.Type) } @@ -224,6 +203,21 @@ func (c *Connector) UnmarshalJSON(b []byte) error { return nil } +// ToStorageConnector converts an object to storage connector type. +func ToStorageConnector(c Connector) (storage.Connector, error) { + data, err := json.Marshal(c.Config) + if err != nil { + return storage.Connector{}, fmt.Errorf("failed to marshal connector config: %v", err) + } + + return storage.Connector{ + ID: c.ID, + Type: c.Type, + Name: c.Name, + Config: data, + }, nil +} + // Expiry holds configuration for the validity period of components. type Expiry struct { // SigningKeys defines the duration of time after which the SigningKeys will be rotated. diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go index 004fec00..f5913cde 100644 --- a/cmd/dex/config_test.go +++ b/cmd/dex/config_test.go @@ -86,7 +86,7 @@ logger: }, }, }, - Connectors: []Connector{ + StaticConnectors: []Connector{ { Type: "mockCallback", ID: "mock", diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index 08d9fdb0..8ee58b77 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -74,7 +74,6 @@ func serve(cmd *cobra.Command, args []string) error { errMsg string }{ {c.Issuer == "", "no issuer specified in config file"}, - {len(c.Connectors) == 0 && !c.EnablePasswordDB, "no connectors supplied in config file"}, {!c.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"}, {c.Storage.Config == nil, "no storage suppied in config file"}, {c.Web.HTTP == "" && c.Web.HTTPS == "", "must supply a HTTP/HTTPS address to listen on"}, @@ -128,34 +127,6 @@ func serve(cmd *cobra.Command, args []string) error { } } - connectors := make([]server.Connector, len(c.Connectors)) - for i, conn := range c.Connectors { - if conn.ID == "" { - return fmt.Errorf("invalid config: no ID field for connector %d", i) - } - if conn.Config == nil { - return fmt.Errorf("invalid config: no config field for connector %q", conn.ID) - } - if conn.Name == "" { - return fmt.Errorf("invalid config: no Name field for connector %q", conn.ID) - } - logger.Infof("config connector: %s", conn.ID) - - connectorLogger := logger.WithField("connector", conn.Name) - c, err := conn.Config.Open(connectorLogger) - if err != nil { - return fmt.Errorf("failed to create connector %s: %v", conn.ID, err) - } - connectors[i] = server.Connector{ - ID: conn.ID, - DisplayName: conn.Name, - Connector: c, - } - } - if c.EnablePasswordDB { - logger.Infof("config connector: local passwords enabled") - } - s, err := c.Storage.Config.Open(logger) if err != nil { return fmt.Errorf("failed to initialize storage: %v", err) @@ -176,6 +147,35 @@ func serve(cmd *cobra.Command, args []string) error { s = storage.WithStaticPasswords(s, passwords) } + if c.EnablePasswordDB { + c.StaticConnectors = append(c.StaticConnectors, Connector{ + ID: server.LocalConnector, + Name: "Email", + Type: server.LocalConnector, + }) + logger.Infof("config connector: local passwords enabled") + } + + storageConnectors := make([]storage.Connector, len(c.StaticConnectors)) + for i, c := range c.StaticConnectors { + if c.ID == "" || c.Name == "" || c.Type == "" { + return fmt.Errorf("invalid config: ID, Type and Name fields are required for a connector") + } + if c.Config == nil { + return fmt.Errorf("invalid config: no config field for connector %q", c.ID) + } + logger.Infof("config connector: %s", c.ID) + + // convert to a storage connector object + conn, err := ToStorageConnector(c) + if err != nil { + return fmt.Errorf("failed to initialize storage connectors: %v", err) + } + storageConnectors[i] = conn + + } + s = storage.WithStaticConnectors(s, storageConnectors) + if len(c.OAuth2.ResponseTypes) > 0 { logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes) } @@ -194,10 +194,8 @@ func serve(cmd *cobra.Command, args []string) error { SkipApprovalScreen: c.OAuth2.SkipApprovalScreen, AllowedOrigins: c.Web.AllowedOrigins, Issuer: c.Issuer, - Connectors: connectors, Storage: s, Web: c.Frontend, - EnablePasswordDB: c.EnablePasswordDB, Logger: logger, Now: now, } diff --git a/server/handlers.go b/server/handlers.go index 78eae26c..683c6c20 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -167,24 +167,31 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) { return } - if len(s.connectors) == 1 { - for id := range s.connectors { + connectors, e := s.storage.ListConnectors() + if e != nil { + s.logger.Errorf("Failed to get list of connectors: %v", err) + s.renderError(w, http.StatusInternalServerError, "Failed to retrieve connector list.") + return + } + + if len(connectors) == 1 { + for _, c := range connectors { // TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter // on create the auth request. - http.Redirect(w, r, s.absPath("/auth", id)+"?req="+authReq.ID, http.StatusFound) + http.Redirect(w, r, s.absPath("/auth", c.ID)+"?req="+authReq.ID, http.StatusFound) return } } - connectorInfos := make([]connectorInfo, len(s.connectors)) + connectorInfos := make([]connectorInfo, len(connectors)) i := 0 - for id, conn := range s.connectors { + for _, conn := range connectors { connectorInfos[i] = connectorInfo{ - ID: id, - Name: conn.DisplayName, + ID: conn.ID, + Name: conn.Name, // TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter // on create the auth request. - URL: s.absPath("/auth", id) + "?req=" + authReq.ID, + URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID, } i++ } @@ -196,10 +203,10 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) { func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) { connID := mux.Vars(r)["connector"] - conn, ok := s.connectors[connID] - if !ok { - s.logger.Errorf("Failed to create authorization request.") - s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.") + conn, err := s.getConnector(connID) + if err != nil { + s.logger.Errorf("Failed to create authorization request: %v", err) + s.renderError(w, http.StatusBadRequest, "Requested resource does not exist") return } @@ -339,8 +346,9 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request) return } - conn, ok := s.connectors[authReq.ConnectorID] - if !ok { + conn, err := s.getConnector(authReq.ConnectorID) + if err != nil { + s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err) s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.") return } @@ -649,13 +657,14 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s // Ensure the connector supports refresh tokens. // // Connectors like `saml` do not implement RefreshConnector. - conn, ok := s.connectors[authCode.ConnectorID] - if !ok { - s.logger.Errorf("connector ID not found: %q", authCode.ConnectorID) + conn, err := s.getConnector(authCode.ConnectorID) + if err != nil { + s.logger.Errorf("connector with ID %q not found: %v", authCode.ConnectorID, err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return false } - _, ok = conn.Connector.(connector.RefreshConnector) + + _, ok := conn.Connector.(connector.RefreshConnector) if !ok { return false } @@ -841,9 +850,9 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie scopes = requestedScopes } - conn, ok := s.connectors[refresh.ConnectorID] - if !ok { - s.logger.Errorf("connector ID not found: %q", refresh.ConnectorID) + conn, err := s.getConnector(refresh.ConnectorID) + if err != nil { + s.logger.Errorf("connector with ID %q not found: %v", refresh.ConnectorID, err) s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) return } diff --git a/server/server.go b/server/server.go index 226558f7..829de4c2 100644 --- a/server/server.go +++ b/server/server.go @@ -2,11 +2,13 @@ package server import ( "context" + "encoding/json" "errors" "fmt" "net/http" "net/url" "path" + "sync" "sync/atomic" "time" @@ -17,14 +19,23 @@ import ( "github.com/gorilla/mux" "github.com/coreos/dex/connector" + "github.com/coreos/dex/connector/github" + "github.com/coreos/dex/connector/gitlab" + "github.com/coreos/dex/connector/ldap" + "github.com/coreos/dex/connector/mock" + "github.com/coreos/dex/connector/oidc" + "github.com/coreos/dex/connector/saml" "github.com/coreos/dex/storage" ) -// Connector is a connector with metadata. +// LocalConnector is the local passwordDB connector which is an internal +// connector maintained by the server. +const LocalConnector = "local" + +// Connector is a connector with resource version metadata. type Connector struct { - ID string - DisplayName string - Connector connector.Connector + ResourceVersion string + Connector connector.Connector } // Config holds the server's configuration options. @@ -36,9 +47,6 @@ type Config struct { // The backing persistence layer. Storage storage.Storage - // Strategies for federated identity. - Connectors []Connector - // Valid values are "code" to enable the code flow and "token" to enable the implicit // flow. If no response types are supplied this value defaults to "code". SupportedResponseTypes []string @@ -60,8 +68,6 @@ type Config struct { // If specified, the server will use this function for determining time. Now func() time.Time - EnablePasswordDB bool - Web WebConfig Logger logrus.FieldLogger @@ -103,7 +109,9 @@ func value(val, defaultValue time.Duration) time.Duration { type Server struct { issuerURL url.URL - // Read-only map of connector IDs to connectors. + // mutex for the connectors map. + mu sync.Mutex + // Map of connector IDs to connectors. connectors map[string]Connector storage storage.Storage @@ -137,17 +145,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) if err != nil { return nil, fmt.Errorf("server: can't parse issuer URL") } - if c.EnablePasswordDB { - c.Connectors = append(c.Connectors, Connector{ - ID: "local", - DisplayName: "Email", - Connector: newPasswordDB(c.Storage), - }) - } - if len(c.Connectors) == 0 { - return nil, errors.New("server: no connectors specified") - } if c.Storage == nil { return nil, errors.New("server: storage cannot be nil") } @@ -195,8 +193,21 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) logger: c.Logger, } - for _, conn := range c.Connectors { - s.connectors[conn.ID] = conn + // Retrieves connector objects in backend storage. This list includes the static connectors + // defined in the ConfigMap and dynamic connectors retrieved from the storage. + storageConnectors, err := c.Storage.ListConnectors() + if err != nil { + return nil, fmt.Errorf("server: failed to list connector objects from storage: %v", err) + } + + if len(storageConnectors) == 0 && len(s.connectors) == 0 { + return nil, errors.New("server: no connectors specified") + } + + for _, conn := range storageConnectors { + if _, err := s.OpenConnector(conn); err != nil { + return nil, fmt.Errorf("server: Failed to open connector %s: %v", conn.ID, err) + } } r := mux.NewRouter() @@ -362,3 +373,99 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura }() return } + +// ConnectorConfig is a configuration that can open a connector. +type ConnectorConfig interface { + Open(logrus.FieldLogger) (connector.Connector, error) +} + +// ConnectorsConfig variable provides an easy way to return a config struct +// depending on the connector type. +var ConnectorsConfig = map[string]func() ConnectorConfig{ + "mockCallback": func() ConnectorConfig { return new(mock.CallbackConfig) }, + "mockPassword": func() ConnectorConfig { return new(mock.PasswordConfig) }, + "ldap": func() ConnectorConfig { return new(ldap.Config) }, + "github": func() ConnectorConfig { return new(github.Config) }, + "gitlab": func() ConnectorConfig { return new(gitlab.Config) }, + "oidc": func() ConnectorConfig { return new(oidc.Config) }, + "saml": func() ConnectorConfig { return new(saml.Config) }, + // Keep around for backwards compatibility. + "samlExperimental": func() ConnectorConfig { return new(saml.Config) }, +} + +// openConnector will parse the connector config and open the connector. +func openConnector(logger logrus.FieldLogger, conn storage.Connector) (connector.Connector, error) { + var c connector.Connector + + f, ok := ConnectorsConfig[conn.Type] + if !ok { + return c, fmt.Errorf("unknown connector type %q", conn.Type) + } + + connConfig := f() + if len(conn.Config) != 0 { + data := []byte(string(conn.Config)) + if err := json.Unmarshal(data, connConfig); err != nil { + return c, fmt.Errorf("parse connector config: %v", err) + } + } + + c, err := connConfig.Open(logger) + if err != nil { + return c, fmt.Errorf("failed to create connector %s: %v", conn.ID, err) + } + + return c, nil +} + +// OpenConnector updates server connector map with specified connector object. +func (s *Server) OpenConnector(conn storage.Connector) (Connector, error) { + var c connector.Connector + + if conn.Type == LocalConnector { + c = newPasswordDB(s.storage) + } else { + var err error + c, err = openConnector(s.logger.WithField("connector", conn.Name), conn) + if err != nil { + return Connector{}, fmt.Errorf("failed to open connector: %v", err) + } + } + + connector := Connector{ + ResourceVersion: conn.ResourceVersion, + Connector: c, + } + s.mu.Lock() + s.connectors[conn.ID] = connector + s.mu.Unlock() + + return connector, nil +} + +// getConnector retrieves the connector object with the given id from the storage +// and updates the connector list for server if necessary. +func (s *Server) getConnector(id string) (Connector, error) { + storageConnector, err := s.storage.GetConnector(id) + if err != nil { + return Connector{}, fmt.Errorf("failed to get connector object from storage: %v", err) + } + + var conn Connector + var ok bool + s.mu.Lock() + conn, ok = s.connectors[id] + s.mu.Unlock() + + if !ok || storageConnector.ResourceVersion != conn.ResourceVersion { + // Connector object does not exist in server connectors map or + // has been updated in the storage. Need to get latest. + conn, err := s.OpenConnector(storageConnector) + if err != nil { + return Connector{}, fmt.Errorf("failed to open connector: %v", err) + } + return conn, nil + } + + return conn, nil +} diff --git a/server/server_test.go b/server/server_test.go index e326e93c..844c7f7b 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -89,13 +89,6 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi config := Config{ Issuer: s.URL, Storage: memory.New(logger), - Connectors: []Connector{ - { - ID: "mock", - DisplayName: "Mock", - Connector: mock.NewCallbackConnector(logger), - }, - }, Web: WebConfig{ Dir: filepath.Join(os.Getenv("GOPATH"), "src/github.com/coreos/dex/web"), }, @@ -106,6 +99,16 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi } s.URL = config.Issuer + connector := storage.Connector{ + ID: "mock", + Type: "mockCallback", + Name: "Mock", + ResourceVersion: "1", + } + if err := config.Storage.CreateConnector(connector); err != nil { + t.Fatalf("create connector: %v", err) + } + var err error if server, err = newServer(ctx, config, staticRotationStrategy(testKey)); err != nil { t.Fatal(err) @@ -416,29 +419,16 @@ func TestOAuth2CodeFlow(t *testing.T) { defer cancel() // Setup a dex server. - logger := &logrus.Logger{ - Out: os.Stderr, - Formatter: &logrus.TextFormatter{DisableColors: true}, - Level: logrus.DebugLevel, - } httpServer, s := newTestServer(ctx, t, func(c *Config) { c.Issuer = c.Issuer + "/non-root-path" c.Now = now c.IDTokensValidFor = idTokensValidFor - - // Testing connector that redirects without interaction with - // the user. - conn = mock.NewCallbackConnector(logger).(*mock.Callback) - c.Connectors = []Connector{ - { - ID: "mock", - DisplayName: "mock", - Connector: conn, - }, - } }) defer httpServer.Close() + mockConn := s.connectors["mock"] + conn = mockConn.Connector.(*mock.Callback) + // Query server's provider metadata. p, err := oidc.NewProvider(ctx, httpServer.URL) if err != nil { diff --git a/storage/conformance/conformance.go b/storage/conformance/conformance.go index 50e705ba..0bfc4745 100644 --- a/storage/conformance/conformance.go +++ b/storage/conformance/conformance.go @@ -628,6 +628,10 @@ func testConnectorCRUD(t *testing.T, s storage.Storage) { c1.Type = "oidc" getAndCompare(id1, c1) + if _, err := s.ListConnectors(); err != nil { + t.Fatalf("failed to list connectors: %v", err) + } + if err := s.DeleteConnector(c1.ID); err != nil { t.Fatalf("failed to delete connector: %v", err) } diff --git a/storage/memory/static_test.go b/storage/memory/static_test.go index 33140612..b29b5ea9 100644 --- a/storage/memory/static_test.go +++ b/storage/memory/static_test.go @@ -190,3 +190,94 @@ func TestStaticPasswords(t *testing.T) { } } } + +func TestStaticConnectors(t *testing.T) { + logger := &logrus.Logger{ + Out: os.Stderr, + Formatter: &logrus.TextFormatter{DisableColors: true}, + Level: logrus.DebugLevel, + } + backing := New(logger) + + config1 := []byte(`{"issuer": "https://accounts.google.com"}`) + config2 := []byte(`{"host": "ldap.example.com:636"}`) + config3 := []byte(`{"issuer": "https://example.com"}`) + + c1 := storage.Connector{ID: storage.NewID(), Type: "oidc", Name: "oidc", ResourceVersion: "1", Config: config1} + c2 := storage.Connector{ID: storage.NewID(), Type: "ldap", Name: "ldap", ResourceVersion: "1", Config: config2} + c3 := storage.Connector{ID: storage.NewID(), Type: "saml", Name: "saml", ResourceVersion: "1", Config: config3} + + backing.CreateConnector(c1) + s := storage.WithStaticConnectors(backing, []storage.Connector{c2}) + + tests := []struct { + name string + action func() error + wantErr bool + }{ + { + name: "get connector from static storage", + action: func() error { + _, err := s.GetConnector(c2.ID) + return err + }, + }, + { + name: "get connector from backing storage", + action: func() error { + _, err := s.GetConnector(c1.ID) + return err + }, + }, + { + name: "update static connector", + action: func() error { + updater := func(c storage.Connector) (storage.Connector, error) { + c.Name = "New" + return c, nil + } + return s.UpdateConnector(c2.ID, updater) + }, + wantErr: true, + }, + { + name: "update non-static connector", + action: func() error { + updater := func(c storage.Connector) (storage.Connector, error) { + c.Name = "New" + return c, nil + } + return s.UpdateConnector(c1.ID, updater) + }, + }, + { + name: "list connectors", + action: func() error { + connectors, err := s.ListConnectors() + if err != nil { + return err + } + if n := len(connectors); n != 2 { + return fmt.Errorf("expected 2 connectors got %d", n) + } + return nil + }, + }, + { + name: "create connector", + action: func() error { + return s.CreateConnector(c3) + }, + }, + } + + for _, tc := range tests { + err := tc.action() + if err != nil && !tc.wantErr { + t.Errorf("%s: %v", tc.name, err) + } + if err == nil && tc.wantErr { + t.Errorf("%s: expected error, didn't get one", tc.name) + } + } +} diff --git a/storage/static.go b/storage/static.go index d5b4f83f..53bd9bfe 100644 --- a/storage/static.go +++ b/storage/static.go @@ -150,3 +150,73 @@ func (s staticPasswordsStorage) UpdatePassword(email string, updater func(old Pa } return s.Storage.UpdatePassword(email, updater) } + +// staticConnectorsStorage represents a storage with read-only set of connectors. +type staticConnectorsStorage struct { + Storage + + // A read-only set of connectors. + connectors []Connector + connectorsByID map[string]Connector +} + +// WithStaticConnectors returns a storage with a read-only set of Connectors. Write actions, +// such as updating existing Connectors, will fail. +func WithStaticConnectors(s Storage, staticConnectors []Connector) Storage { + connectorsByID := make(map[string]Connector, len(staticConnectors)) + for _, c := range staticConnectors { + connectorsByID[c.ID] = c + } + return staticConnectorsStorage{s, staticConnectors, connectorsByID} +} + +func (s staticConnectorsStorage) isStatic(id string) bool { + _, ok := s.connectorsByID[id] + return ok +} + +func (s staticConnectorsStorage) GetConnector(id string) (Connector, error) { + if connector, ok := s.connectorsByID[id]; ok { + return connector, nil + } + return s.Storage.GetConnector(id) +} + +func (s staticConnectorsStorage) ListConnectors() ([]Connector, error) { + connectors, err := s.Storage.ListConnectors() + if err != nil { + return nil, err + } + + n := 0 + for _, connector := range connectors { + // If an entry has the same id as those provided in the static + // values, prefer the static value. + if !s.isStatic(connector.ID) { + connectors[n] = connector + n++ + } + } + return append(connectors[:n], s.connectors...), nil +} + +func (s staticConnectorsStorage) CreateConnector(c Connector) error { + if s.isStatic(c.ID) { + return errors.New("static connectors: read-only cannot create connector") + } + return s.Storage.CreateConnector(c) +} + +func (s staticConnectorsStorage) DeleteConnector(id string) error { + if s.isStatic(id) { + return errors.New("static connectors: read-only cannot delete connector") + } + return s.Storage.DeleteConnector(id) +} + +func (s staticConnectorsStorage) UpdateConnector(id string, updater func(old Connector) (Connector, error)) error { + if s.isStatic(id) { + return errors.New("static connectors: read-only cannot update connector") + } + return s.Storage.UpdateConnector(id, updater) +} diff --git a/storage/storage.go b/storage/storage.go index 92ac2ee2..8ea5ab2c 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -298,17 +298,17 @@ type Password struct { // Connector is an object that contains the metadata about connectors used to login to Dex. type Connector struct { // ID that will uniquely identify the connector object. - ID string + ID string `json:"id"` // The Type of the connector. E.g. 'oidc' or 'ldap' - Type string + Type string `json:"type"` // The Name of the connector that is used when displaying it to the end user. - Name string + Name string `json:"name"` // ResourceVersion is the static versioning used to keep track of dynamic configuration // changes to the connector object made by the API calls. - ResourceVersion string + ResourceVersion string `json:"resourceVersion"` // Config holds all the configuration information specific to the connector type. Since there // no generic struct we can use for this purpose, it is stored as a byte stream. - Config []byte + Config []byte `json:"email"` } // VerificationKey is a rotated signing key which can still be used to verify