From 2909929b17ca6606e85380a54c88b98b0117971d Mon Sep 17 00:00:00 2001
From: Eric Chiang <eric.chiang@coreos.com>
Date: Wed, 5 Oct 2016 16:50:02 -0700
Subject: [PATCH] *: add the ability to define passwords statically

---
 TODO.md                                  |  3 +-
 cmd/dex/config.go                        | 40 +++++++++++++++++++++++
 cmd/dex/serve.go                         | 13 +++++++-
 examples/config-dev.yaml                 | 19 +++++++----
 storage/{static_clients.go => static.go} | 41 +++++++++++++++++++++++-
 5 files changed, 107 insertions(+), 9 deletions(-)
 rename storage/{static_clients.go => static.go} (58%)

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")
+}
-- 
GitLab