server: account for dynamically changing connector object in storage.
This commit is contained in:
parent
2b8caf9b39
commit
8c9c2518f5
10 changed files with 400 additions and 137 deletions
|
@ -9,13 +9,6 @@ import (
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"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/server"
|
||||||
"github.com/coreos/dex/storage"
|
"github.com/coreos/dex/storage"
|
||||||
"github.com/coreos/dex/storage/kubernetes"
|
"github.com/coreos/dex/storage/kubernetes"
|
||||||
|
@ -25,17 +18,20 @@ import (
|
||||||
|
|
||||||
// Config is the config format for the main application.
|
// Config is the config format for the main application.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
Storage Storage `json:"storage"`
|
Storage Storage `json:"storage"`
|
||||||
Connectors []Connector `json:"connectors"`
|
Web Web `json:"web"`
|
||||||
Web Web `json:"web"`
|
OAuth2 OAuth2 `json:"oauth2"`
|
||||||
OAuth2 OAuth2 `json:"oauth2"`
|
GRPC GRPC `json:"grpc"`
|
||||||
GRPC GRPC `json:"grpc"`
|
Expiry Expiry `json:"expiry"`
|
||||||
Expiry Expiry `json:"expiry"`
|
Logger Logger `json:"logger"`
|
||||||
Logger Logger `json:"logger"`
|
|
||||||
|
|
||||||
Frontend server.WebConfig `json:"frontend"`
|
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
|
// StaticClients cause the server to use this list of clients rather than
|
||||||
// querying the storage. Write operations, like creating a client, will fail.
|
// querying the storage. Write operations, like creating a client, will fail.
|
||||||
StaticClients []storage.Client `json:"staticClients"`
|
StaticClients []storage.Client `json:"staticClients"`
|
||||||
|
@ -170,24 +166,7 @@ type Connector struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
||||||
Config ConnectorConfig `json:"config"`
|
Config server.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) },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON allows Connector to implement the unmarshaler interface to
|
// 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 {
|
if err := json.Unmarshal(b, &conn); err != nil {
|
||||||
return fmt.Errorf("parse connector: %v", err)
|
return fmt.Errorf("parse connector: %v", err)
|
||||||
}
|
}
|
||||||
f, ok := connectors[conn.Type]
|
f, ok := server.ConnectorsConfig[conn.Type]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown connector type %q", conn.Type)
|
return fmt.Errorf("unknown connector type %q", conn.Type)
|
||||||
}
|
}
|
||||||
|
@ -224,6 +203,21 @@ func (c *Connector) UnmarshalJSON(b []byte) error {
|
||||||
return nil
|
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.
|
// Expiry holds configuration for the validity period of components.
|
||||||
type Expiry struct {
|
type Expiry struct {
|
||||||
// SigningKeys defines the duration of time after which the SigningKeys will be rotated.
|
// SigningKeys defines the duration of time after which the SigningKeys will be rotated.
|
||||||
|
|
|
@ -86,7 +86,7 @@ logger:
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Connectors: []Connector{
|
StaticConnectors: []Connector{
|
||||||
{
|
{
|
||||||
Type: "mockCallback",
|
Type: "mockCallback",
|
||||||
ID: "mock",
|
ID: "mock",
|
||||||
|
|
|
@ -74,7 +74,6 @@ func serve(cmd *cobra.Command, args []string) error {
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
{c.Issuer == "", "no issuer specified in config file"},
|
{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.EnablePasswordDB && len(c.StaticPasswords) != 0, "cannot specify static passwords without enabling password db"},
|
||||||
{c.Storage.Config == nil, "no storage suppied in config file"},
|
{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"},
|
{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)
|
s, err := c.Storage.Config.Open(logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize storage: %v", err)
|
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)
|
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 {
|
if len(c.OAuth2.ResponseTypes) > 0 {
|
||||||
logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
|
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,
|
SkipApprovalScreen: c.OAuth2.SkipApprovalScreen,
|
||||||
AllowedOrigins: c.Web.AllowedOrigins,
|
AllowedOrigins: c.Web.AllowedOrigins,
|
||||||
Issuer: c.Issuer,
|
Issuer: c.Issuer,
|
||||||
Connectors: connectors,
|
|
||||||
Storage: s,
|
Storage: s,
|
||||||
Web: c.Frontend,
|
Web: c.Frontend,
|
||||||
EnablePasswordDB: c.EnablePasswordDB,
|
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Now: now,
|
Now: now,
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,24 +167,31 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.connectors) == 1 {
|
connectors, e := s.storage.ListConnectors()
|
||||||
for id := range s.connectors {
|
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
|
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
|
||||||
// on create the auth request.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectorInfos := make([]connectorInfo, len(s.connectors))
|
connectorInfos := make([]connectorInfo, len(connectors))
|
||||||
i := 0
|
i := 0
|
||||||
for id, conn := range s.connectors {
|
for _, conn := range connectors {
|
||||||
connectorInfos[i] = connectorInfo{
|
connectorInfos[i] = connectorInfo{
|
||||||
ID: id,
|
ID: conn.ID,
|
||||||
Name: conn.DisplayName,
|
Name: conn.Name,
|
||||||
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
|
// TODO(ericchiang): Make this pass on r.URL.RawQuery and let something latter
|
||||||
// on create the auth request.
|
// on create the auth request.
|
||||||
URL: s.absPath("/auth", id) + "?req=" + authReq.ID,
|
URL: s.absPath("/auth", conn.ID) + "?req=" + authReq.ID,
|
||||||
}
|
}
|
||||||
i++
|
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) {
|
func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
connID := mux.Vars(r)["connector"]
|
connID := mux.Vars(r)["connector"]
|
||||||
conn, ok := s.connectors[connID]
|
conn, err := s.getConnector(connID)
|
||||||
if !ok {
|
if err != nil {
|
||||||
s.logger.Errorf("Failed to create authorization request.")
|
s.logger.Errorf("Failed to create authorization request: %v", err)
|
||||||
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist.")
|
s.renderError(w, http.StatusBadRequest, "Requested resource does not exist")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,8 +346,9 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, ok := s.connectors[authReq.ConnectorID]
|
conn, err := s.getConnector(authReq.ConnectorID)
|
||||||
if !ok {
|
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.")
|
s.renderError(w, http.StatusInternalServerError, "Requested resource does not exist.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -649,13 +657,14 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
|
||||||
// Ensure the connector supports refresh tokens.
|
// Ensure the connector supports refresh tokens.
|
||||||
//
|
//
|
||||||
// Connectors like `saml` do not implement RefreshConnector.
|
// Connectors like `saml` do not implement RefreshConnector.
|
||||||
conn, ok := s.connectors[authCode.ConnectorID]
|
conn, err := s.getConnector(authCode.ConnectorID)
|
||||||
if !ok {
|
if err != nil {
|
||||||
s.logger.Errorf("connector ID not found: %q", authCode.ConnectorID)
|
s.logger.Errorf("connector with ID %q not found: %v", authCode.ConnectorID, err)
|
||||||
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, ok = conn.Connector.(connector.RefreshConnector)
|
|
||||||
|
_, ok := conn.Connector.(connector.RefreshConnector)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -841,9 +850,9 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
|
||||||
scopes = requestedScopes
|
scopes = requestedScopes
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, ok := s.connectors[refresh.ConnectorID]
|
conn, err := s.getConnector(refresh.ConnectorID)
|
||||||
if !ok {
|
if err != nil {
|
||||||
s.logger.Errorf("connector ID not found: %q", refresh.ConnectorID)
|
s.logger.Errorf("connector with ID %q not found: %v", refresh.ConnectorID, err)
|
||||||
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
151
server/server.go
151
server/server.go
|
@ -2,11 +2,13 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -17,14 +19,23 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
"github.com/coreos/dex/connector"
|
"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"
|
"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 {
|
type Connector struct {
|
||||||
ID string
|
ResourceVersion string
|
||||||
DisplayName string
|
Connector connector.Connector
|
||||||
Connector connector.Connector
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config holds the server's configuration options.
|
// Config holds the server's configuration options.
|
||||||
|
@ -36,9 +47,6 @@ type Config struct {
|
||||||
// The backing persistence layer.
|
// The backing persistence layer.
|
||||||
Storage storage.Storage
|
Storage storage.Storage
|
||||||
|
|
||||||
// Strategies for federated identity.
|
|
||||||
Connectors []Connector
|
|
||||||
|
|
||||||
// Valid values are "code" to enable the code flow and "token" to enable the implicit
|
// 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".
|
// flow. If no response types are supplied this value defaults to "code".
|
||||||
SupportedResponseTypes []string
|
SupportedResponseTypes []string
|
||||||
|
@ -60,8 +68,6 @@ type Config struct {
|
||||||
// If specified, the server will use this function for determining time.
|
// If specified, the server will use this function for determining time.
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
|
|
||||||
EnablePasswordDB bool
|
|
||||||
|
|
||||||
Web WebConfig
|
Web WebConfig
|
||||||
|
|
||||||
Logger logrus.FieldLogger
|
Logger logrus.FieldLogger
|
||||||
|
@ -103,7 +109,9 @@ func value(val, defaultValue time.Duration) time.Duration {
|
||||||
type Server struct {
|
type Server struct {
|
||||||
issuerURL url.URL
|
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
|
connectors map[string]Connector
|
||||||
|
|
||||||
storage storage.Storage
|
storage storage.Storage
|
||||||
|
@ -137,17 +145,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("server: can't parse issuer URL")
|
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 {
|
if c.Storage == nil {
|
||||||
return nil, errors.New("server: storage cannot be 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,
|
logger: c.Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, conn := range c.Connectors {
|
// Retrieves connector objects in backend storage. This list includes the static connectors
|
||||||
s.connectors[conn.ID] = conn
|
// 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()
|
r := mux.NewRouter()
|
||||||
|
@ -362,3 +373,99 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
|
||||||
}()
|
}()
|
||||||
return
|
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{
|
config := Config{
|
||||||
Issuer: s.URL,
|
Issuer: s.URL,
|
||||||
Storage: memory.New(logger),
|
Storage: memory.New(logger),
|
||||||
Connectors: []Connector{
|
|
||||||
{
|
|
||||||
ID: "mock",
|
|
||||||
DisplayName: "Mock",
|
|
||||||
Connector: mock.NewCallbackConnector(logger),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Web: WebConfig{
|
Web: WebConfig{
|
||||||
Dir: filepath.Join(os.Getenv("GOPATH"), "src/github.com/coreos/dex/web"),
|
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
|
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
|
var err error
|
||||||
if server, err = newServer(ctx, config, staticRotationStrategy(testKey)); err != nil {
|
if server, err = newServer(ctx, config, staticRotationStrategy(testKey)); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
@ -416,29 +419,16 @@ func TestOAuth2CodeFlow(t *testing.T) {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Setup a dex server.
|
// 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) {
|
httpServer, s := newTestServer(ctx, t, func(c *Config) {
|
||||||
c.Issuer = c.Issuer + "/non-root-path"
|
c.Issuer = c.Issuer + "/non-root-path"
|
||||||
c.Now = now
|
c.Now = now
|
||||||
c.IDTokensValidFor = idTokensValidFor
|
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()
|
defer httpServer.Close()
|
||||||
|
|
||||||
|
mockConn := s.connectors["mock"]
|
||||||
|
conn = mockConn.Connector.(*mock.Callback)
|
||||||
|
|
||||||
// Query server's provider metadata.
|
// Query server's provider metadata.
|
||||||
p, err := oidc.NewProvider(ctx, httpServer.URL)
|
p, err := oidc.NewProvider(ctx, httpServer.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -628,6 +628,10 @@ func testConnectorCRUD(t *testing.T, s storage.Storage) {
|
||||||
c1.Type = "oidc"
|
c1.Type = "oidc"
|
||||||
getAndCompare(id1, c1)
|
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 {
|
if err := s.DeleteConnector(c1.ID); err != nil {
|
||||||
t.Fatalf("failed to delete connector: %v", err)
|
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)
|
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.
|
// Connector is an object that contains the metadata about connectors used to login to Dex.
|
||||||
type Connector struct {
|
type Connector struct {
|
||||||
// ID that will uniquely identify the connector object.
|
// ID that will uniquely identify the connector object.
|
||||||
ID string
|
ID string `json:"id"`
|
||||||
// The Type of the connector. E.g. 'oidc' or 'ldap'
|
// 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.
|
// 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
|
// ResourceVersion is the static versioning used to keep track of dynamic configuration
|
||||||
// changes to the connector object made by the API calls.
|
// 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
|
// 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.
|
// 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
|
// VerificationKey is a rotated signing key which can still be used to verify
|
||||||
|
|
Reference in a new issue