Merge pull request #913 from rithujohn191/dynamic-connector
server: account for dynamically changing connector object in storage.
This commit is contained in:
		| @@ -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. | ||||
|   | ||||
| @@ -86,7 +86,7 @@ logger: | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Connectors: []Connector{ | ||||
| 		StaticConnectors: []Connector{ | ||||
| 			{ | ||||
| 				Type:   "mockCallback", | ||||
| 				ID:     "mock", | ||||
|   | ||||
| @@ -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, | ||||
| 	} | ||||
|   | ||||
| @@ -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 | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										151
									
								
								server/server.go
									
									
									
									
									
								
							
							
						
						
									
										151
									
								
								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 | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user