diff --git a/TODO.md b/TODO.md index 58196109..03e9bc52 100644 --- a/TODO.md +++ b/TODO.md @@ -33,7 +33,7 @@ Documentation Storage -- [ ] Add SQL storage implementation +- [x] Add SQL storage implementation - [ ] Utilize fixes for third party resources in Kubernetes 1.4 UX @@ -48,3 +48,4 @@ Backend - [ ] Improve logging, possibly switch to logrus - [ ] Standardize OAuth2 error handling +- [ ] Switch to github.com/ghodss/yaml for []byte to base64 string logic diff --git a/cmd/dex/config.go b/cmd/dex/config.go index f50cd173..482075f3 100644 --- a/cmd/dex/config.go +++ b/cmd/dex/config.go @@ -1,6 +1,7 @@ package main import ( + "encoding/base64" "fmt" "github.com/coreos/dex/connector" @@ -26,7 +27,46 @@ type Config struct { Templates server.TemplateConfig `yaml:"templates"` + // 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 `yaml:"staticClients"` + + // If enabled, the server will maintain a list of passwords which can be used + // to identify a user. + EnablePasswordDB bool `yaml:"enablePasswordDB"` + + // StaticPasswords cause the server use this list of passwords rather than + // querying the storage. Cannot be specified without enabling a passwords + // database. + // + // The "password" type is identical to the storage.Password type, but does + // unmarshaling into []byte correctly. + StaticPasswords []password `yaml:"staticPasswords"` +} + +type password struct { + Email string `yaml:"email"` + Username string `yaml:"username"` + UserID string `yaml:"userID"` + + // Because our YAML parser doesn't base64, we have to do it ourselves. + // + // TODO(ericchiang): switch to github.com/ghodss/yaml + Hash string `yaml:"hash"` +} + +// decode the hash appropriately and convert to the storage passwords. +func (p password) toPassword() (storage.Password, error) { + hash, err := base64.StdEncoding.DecodeString(p.Hash) + if err != nil { + return storage.Password{}, fmt.Errorf("decoding hash: %v", err) + } + return storage.Password{ + Email: p.Email, + Username: p.Username, + UserID: p.UserID, + Hash: hash, + }, nil } // OAuth2 describes enabled OAuth2 extensions. diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index 08309e0f..0e35e6af 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -55,7 +55,8 @@ func serve(cmd *cobra.Command, args []string) error { errMsg string }{ {c.Issuer == "", "no issuer specified in config file"}, - {len(c.Connectors) == 0, "no connectors supplied 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"}, {c.Web.HTTPS != "" && c.Web.TLSCert == "", "no cert specified for HTTPS"}, @@ -103,6 +104,15 @@ func serve(cmd *cobra.Command, args []string) error { if len(c.StaticClients) > 0 { s = storage.WithStaticClients(s, c.StaticClients) } + if len(c.StaticPasswords) > 0 { + p := make([]storage.Password, len(c.StaticPasswords)) + for i, pw := range c.StaticPasswords { + if p[i], err = pw.toPassword(); err != nil { + return err + } + } + s = storage.WithStaticPasswords(s, p) + } serverConfig := server.Config{ SupportedResponseTypes: c.OAuth2.ResponseTypes, @@ -110,6 +120,7 @@ func serve(cmd *cobra.Command, args []string) error { Connectors: connectors, Storage: s, TemplateConfig: c.Templates, + EnablePasswordDB: c.EnablePasswordDB, } serv, err := server.NewServer(serverConfig) diff --git a/examples/config-dev.yaml b/examples/config-dev.yaml index 2a2736b5..d771bb7c 100644 --- a/examples/config-dev.yaml +++ b/examples/config-dev.yaml @@ -11,16 +11,23 @@ connectors: - type: mockCallback id: mock-callback name: Mock -- type: mockPassword - id: mock-password - name: Password - config: - username: "admin" - password: "PASSWORD" +# Instead of reading from an external storage, use this list of clients. staticClients: - id: example-app redirectURIs: - 'http://127.0.0.1:5555/callback' name: 'Example App' secret: ZXhhbXBsZS1hcHAtc2VjcmV0 + +# Let dex keep a list of passwords which can be used to login the user. +enablePasswordDB: true + +# A static list of passwords to login the end user. By identifying here, dex +# won't look in its undlying storage for passwords. +staticPasswords: +- email: "admin@example.com" + # bcrypt hash of the string "password" + hash: "JDJhJDE0JDh4TnlVZ3pzSmVuQm4ySlRPT2QvbmVGcUlnQzF4TEFVRFA3VlpTVzhDNWlkLnFPcmNlYUJX" + username: "admin" + userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" diff --git a/storage/static_clients.go b/storage/static.go similarity index 58% rename from storage/static_clients.go rename to storage/static.go index d7932393..8274c5f8 100644 --- a/storage/static_clients.go +++ b/storage/static.go @@ -1,6 +1,9 @@ package storage -import "errors" +import ( + "errors" + "strings" +) // Tests for this code are in the "memory" package, since this package doesn't // define a concrete storage implementation. @@ -53,3 +56,39 @@ func (s staticClientsStorage) DeleteClient(id string) error { func (s staticClientsStorage) UpdateClient(id string, updater func(old Client) (Client, error)) error { return errors.New("static clients: read-only cannot update client") } + +type staticPasswordsStorage struct { + Storage + + passwordsByEmail map[string]Password +} + +// WithStaticPasswords returns a storage with a read-only set of passwords. Write actions, +// such as creating other passwords, will fail. +func WithStaticPasswords(s Storage, staticPasswords []Password) Storage { + passwordsByEmail := make(map[string]Password, len(staticPasswords)) + for _, p := range staticPasswords { + p.Email = strings.ToLower(p.Email) + passwordsByEmail[p.Email] = p + } + return staticPasswordsStorage{s, passwordsByEmail} +} + +func (s staticPasswordsStorage) GetPassword(email string) (Password, error) { + if password, ok := s.passwordsByEmail[strings.ToLower(email)]; ok { + return password, nil + } + return Password{}, ErrNotFound +} + +func (s staticPasswordsStorage) CreatePassword(p Password) error { + return errors.New("static passwords: read-only cannot create password") +} + +func (s staticPasswordsStorage) DeletePassword(id string) error { + return errors.New("static passwords: read-only cannot create password") +} + +func (s staticPasswordsStorage) UpdatePassword(id string, updater func(old Password) (Password, error)) error { + return errors.New("static passwords: read-only cannot update password") +}