Commit ac73d3cd authored by Eric Chiang's avatar Eric Chiang

*: load password infos from users file in no-db mode not connectors

In --no-db mode, load passwords from the users file instead of the
connectors file. This allows us to remove the password infos field
from the local connector and stop loading them during connector
registration, a case that was causing panics when using a real
database (see #286).

Fixes #286
Closes #340
parent eb6dcead
...@@ -60,23 +60,9 @@ func TestNewConnectorConfigFromMap(t *testing.T) { ...@@ -60,23 +60,9 @@ func TestNewConnectorConfigFromMap(t *testing.T) {
m: map[string]interface{}{ m: map[string]interface{}{
"type": "local", "type": "local",
"id": "foo", "id": "foo",
"passwordInfos": []map[string]string{
{"userId": "abc", "passwordHash": "UElORw=="}, // []byte is base64 encoded when using json.Marshasl
{"userId": "271", "passwordPlaintext": "pong"},
},
}, },
want: &LocalConnectorConfig{ want: &LocalConnectorConfig{
ID: "foo", ID: "foo",
PasswordInfos: []user.PasswordInfo{
user.PasswordInfo{
UserID: "abc",
Password: user.Password("PING"),
},
user.PasswordInfo{
UserID: "271",
Password: user.Password("PONG"),
},
},
}, },
}, },
{ {
...@@ -111,12 +97,6 @@ func TestNewConnectorConfigFromMap(t *testing.T) { ...@@ -111,12 +97,6 @@ func TestNewConnectorConfigFromMap(t *testing.T) {
func TestNewConnectorConfigFromMapFail(t *testing.T) { func TestNewConnectorConfigFromMapFail(t *testing.T) {
tests := []map[string]interface{}{ tests := []map[string]interface{}{
// invalid local connector
map[string]interface{}{
"type": "local",
"passwordInfos": "invalid",
},
// no type // no type
map[string]interface{}{ map[string]interface{}{
"id": "bar", "id": "bar",
......
...@@ -22,7 +22,6 @@ func init() { ...@@ -22,7 +22,6 @@ func init() {
type LocalConnectorConfig struct { type LocalConnectorConfig struct {
ID string `json:"id"` ID string `json:"id"`
PasswordInfos []user.PasswordInfo `json:"passwordInfos"`
} }
func (cfg *LocalConnectorConfig) ConnectorID() string { func (cfg *LocalConnectorConfig) ConnectorID() string {
......
...@@ -113,7 +113,7 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) { ...@@ -113,7 +113,7 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
} }
cfg := &connector.LocalConnectorConfig{ cfg := &connector.LocalConnectorConfig{
PasswordInfos: []user.PasswordInfo{passwordInfo}, ID: "local",
} }
ci := oidc.ClientIdentity{ ci := oidc.ClientIdentity{
...@@ -128,6 +128,10 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) { ...@@ -128,6 +128,10 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to create client identity repo: " + err.Error()) t.Fatalf("Failed to create client identity repo: " + err.Error())
} }
passwordInfoRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(db.NewMemDB(), []user.PasswordInfo{passwordInfo})
if err != nil {
t.Fatalf("Failed to create password info repo: %v", err)
}
issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} issuerURL := url.URL{Scheme: "http", Host: "server.example.com"}
sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap)) sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap))
...@@ -153,7 +157,6 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) { ...@@ -153,7 +157,6 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
passwordInfoRepo := db.NewPasswordInfoRepo(db.NewMemDB())
refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo()
srv := &server.Server{ srv := &server.Server{
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"io"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
...@@ -136,7 +137,7 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error { ...@@ -136,7 +137,7 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
skRepo := db.NewSessionKeyRepo(dbMap) skRepo := db.NewSessionKeyRepo(dbMap)
sm := sessionmanager.NewSessionManager(sRepo, skRepo) sm := sessionmanager.NewSessionManager(sRepo, skRepo)
users, err := loadUsers(cfg.UsersFile) users, pwis, err := loadUsers(cfg.UsersFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to read users from file: %v", err) return fmt.Errorf("unable to read users from file: %v", err)
} }
...@@ -145,7 +146,10 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error { ...@@ -145,7 +146,10 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
return err return err
} }
pwiRepo := db.NewPasswordInfoRepo(dbMap) pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis)
if err != nil {
return err
}
refTokRepo := db.NewRefreshTokenRepo(dbMap) refTokRepo := db.NewRefreshTokenRepo(dbMap)
...@@ -163,28 +167,61 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error { ...@@ -163,28 +167,61 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
return nil return nil
} }
func loadUsers(filepath string) (users []user.UserWithRemoteIdentities, err error) { // loadUsers parses the user.json file and returns the users to be created.
func loadUsers(filepath string) ([]user.UserWithRemoteIdentities, []user.PasswordInfo, error) {
f, err := os.Open(filepath) f, err := os.Open(filepath)
if err != nil { if err != nil {
return return nil, nil, err
} }
defer f.Close() defer f.Close()
err = json.NewDecoder(f).Decode(&users) return loadUsersFromReader(f)
}
func loadUsersFromReader(r io.Reader) (users []user.UserWithRemoteIdentities, pwis []user.PasswordInfo, err error) {
// Encoding used by the user config file.
var configUsers []struct {
user.User
Password string `json:"password"`
RemoteIdentities []user.RemoteIdentity `json:"remoteIdentities"`
}
if err := json.NewDecoder(r).Decode(&configUsers); err != nil {
return nil, nil, err
}
users = make([]user.UserWithRemoteIdentities, len(configUsers))
pwis = make([]user.PasswordInfo, len(configUsers))
for i, u := range configUsers {
users[i] = user.UserWithRemoteIdentities{
User: u.User,
RemoteIdentities: u.RemoteIdentities,
}
hashedPassword, err := user.NewPasswordFromPlaintext(u.Password)
if err != nil {
return nil, nil, err
}
pwis[i] = user.PasswordInfo{UserID: u.ID, Password: hashedPassword}
}
return return
} }
// loadClients parses the clients.json file and returns the clients to be created.
func loadClients(filepath string) ([]oidc.ClientIdentity, error) { func loadClients(filepath string) ([]oidc.ClientIdentity, error) {
f, err := os.Open(filepath) f, err := os.Open(filepath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer f.Close() defer f.Close()
return loadClientsFromReader(f)
}
func loadClientsFromReader(r io.Reader) ([]oidc.ClientIdentity, error) {
var c []struct { var c []struct {
ID string `json:"id"` ID string `json:"id"`
Secret string `json:"secret"` Secret string `json:"secret"`
RedirectURLs []string `json:"redirectURLs"` RedirectURLs []string `json:"redirectURLs"`
} }
if err := json.NewDecoder(f).Decode(&c); err != nil { if err := json.NewDecoder(r).Decode(&c); err != nil {
return nil, err return nil, err
} }
clients := make([]oidc.ClientIdentity, len(c)) clients := make([]oidc.ClientIdentity, len(c))
......
package server
import (
"strings"
"testing"
"github.com/coreos/dex/user"
"github.com/kylelemons/godebug/pretty"
)
func TestLoadUsers(t *testing.T) {
tests := []struct {
// The raw JSON file
raw string
expUsers []user.UserWithRemoteIdentities
// userid -> plaintext password
expPasswds map[string]string
}{
{
raw: `[
{
"id": "elroy-id",
"email": "elroy77@example.com",
"displayName": "Elroy Jonez",
"password": "bones",
"remoteIdentities": [
{
"connectorId": "local",
"id": "elroy-id"
}
]
}
]`,
expUsers: []user.UserWithRemoteIdentities{
{
User: user.User{
ID: "elroy-id",
Email: "elroy77@example.com",
DisplayName: "Elroy Jonez",
},
RemoteIdentities: []user.RemoteIdentity{
{
ConnectorID: "local",
ID: "elroy-id",
},
},
},
},
expPasswds: map[string]string{
"elroy-id": "bones",
},
},
}
for i, tt := range tests {
users, pwInfos, err := loadUsersFromReader(strings.NewReader(tt.raw))
if err != nil {
t.Errorf("case %d: failed to load user: %v", i, err)
return
}
if diff := pretty.Compare(tt.expUsers, users); diff != "" {
t.Errorf("case: %d: wantUsers!=gotUsers: %s", i, diff)
}
// For each password info loaded, verify the password.
for _, pwInfo := range pwInfos {
expPW, ok := tt.expPasswds[pwInfo.UserID]
if !ok {
t.Errorf("no password entry for %s", pwInfo.UserID)
continue
}
if _, err := pwInfo.Authenticate(expPW); err != nil {
t.Errorf("case %d: user %s's password did not match", i, pwInfo.UserID)
}
}
}
}
...@@ -178,19 +178,6 @@ func (s *Server) AddConnector(cfg connector.ConnectorConfig) error { ...@@ -178,19 +178,6 @@ func (s *Server) AddConnector(cfg connector.ConnectorConfig) error {
UserRepo: s.UserRepo, UserRepo: s.UserRepo,
PasswordInfoRepo: s.PasswordInfoRepo, PasswordInfoRepo: s.PasswordInfoRepo,
}) })
localCfg, ok := cfg.(*connector.LocalConnectorConfig)
if !ok {
return errors.New("config for LocalConnector not a LocalConnectorConfig?")
}
if len(localCfg.PasswordInfos) > 0 {
err := user.LoadPasswordInfos(s.PasswordInfoRepo,
localCfg.PasswordInfos)
if err != nil {
return err
}
}
} }
log.Infof("Loaded IdP connector: id=%s type=%s", connectorID, cfg.ConnectorType()) log.Infof("Loaded IdP connector: id=%s type=%s", connectorID, cfg.ConnectorType())
......
[ [
{ {
"type": "local", "type": "local",
"id": "local", "id": "local"
"passwordInfos": [
{
"userId":"elroy-id",
"passwordPlaintext": "bones"
},
{
"userId":"penny",
"passwordPlaintext": "kibble"
}
]
}, },
{ {
"type": "oidc", "type": "oidc",
......
[ [
{ {
"user":{
"id": "elroy-id", "id": "elroy-id",
"email": "elroy77@example.com", "email": "elroy77@example.com",
"displayName": "Elroy Jonez" "displayName": "Elroy Jonez",
}, "password": "bones",
"remoteIdentities": [ "remoteIdentities": [
{ {
"connectorId": "local", "connectorId": "local",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment