*: 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
This commit is contained in:
Eric Chiang 2016-04-04 18:05:00 -07:00
parent eb6dceadfd
commit ac73d3cdf2
8 changed files with 132 additions and 59 deletions

View file

@ -60,23 +60,9 @@ func TestNewConnectorConfigFromMap(t *testing.T) {
m: map[string]interface{}{
"type": "local",
"id": "foo",
"passwordInfos": []map[string]string{
{"userId": "abc", "passwordHash": "UElORw=="}, // []byte is base64 encoded when using json.Marshasl
{"userId": "271", "passwordPlaintext": "pong"},
},
},
want: &LocalConnectorConfig{
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) {
func TestNewConnectorConfigFromMapFail(t *testing.T) {
tests := []map[string]interface{}{
// invalid local connector
map[string]interface{}{
"type": "local",
"passwordInfos": "invalid",
},
// no type
map[string]interface{}{
"id": "bar",

View file

@ -21,8 +21,7 @@ func init() {
}
type LocalConnectorConfig struct {
ID string `json:"id"`
PasswordInfos []user.PasswordInfo `json:"passwordInfos"`
ID string `json:"id"`
}
func (cfg *LocalConnectorConfig) ConnectorID() string {

View file

@ -113,7 +113,7 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
}
cfg := &connector.LocalConnectorConfig{
PasswordInfos: []user.PasswordInfo{passwordInfo},
ID: "local",
}
ci := oidc.ClientIdentity{
@ -128,6 +128,10 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
if err != nil {
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"}
sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap))
@ -153,7 +157,6 @@ func TestHTTPExchangeTokenRefreshToken(t *testing.T) {
t.Fatalf("Unexpected error: %v", err)
}
passwordInfoRepo := db.NewPasswordInfoRepo(db.NewMemDB())
refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo()
srv := &server.Server{

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"html/template"
"io"
"net/url"
"os"
"path/filepath"
@ -136,7 +137,7 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
skRepo := db.NewSessionKeyRepo(dbMap)
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
users, err := loadUsers(cfg.UsersFile)
users, pwis, err := loadUsers(cfg.UsersFile)
if err != nil {
return fmt.Errorf("unable to read users from file: %v", err)
}
@ -145,7 +146,10 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
return err
}
pwiRepo := db.NewPasswordInfoRepo(dbMap)
pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis)
if err != nil {
return err
}
refTokRepo := db.NewRefreshTokenRepo(dbMap)
@ -163,28 +167,61 @@ func (cfg *SingleServerConfig) Configure(srv *Server) error {
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)
if err != nil {
return
return nil, nil, err
}
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
}
// loadClients parses the clients.json file and returns the clients to be created.
func loadClients(filepath string) ([]oidc.ClientIdentity, error) {
f, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer f.Close()
return loadClientsFromReader(f)
}
func loadClientsFromReader(r io.Reader) ([]oidc.ClientIdentity, error) {
var c []struct {
ID string `json:"id"`
Secret string `json:"secret"`
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
}
clients := make([]oidc.ClientIdentity, len(c))

78
server/config_test.go Normal file
View file

@ -0,0 +1,78 @@
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)
}
}
}
}

View file

@ -178,19 +178,6 @@ func (s *Server) AddConnector(cfg connector.ConnectorConfig) error {
UserRepo: s.UserRepo,
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())

View file

@ -1,17 +1,7 @@
[
{
"type": "local",
"id": "local",
"passwordInfos": [
{
"userId":"elroy-id",
"passwordPlaintext": "bones"
},
{
"userId":"penny",
"passwordPlaintext": "kibble"
}
]
"id": "local"
},
{
"type": "oidc",

View file

@ -1,10 +1,9 @@
[
{
"user":{
"id": "elroy-id",
"email": "elroy77@example.com",
"displayName": "Elroy Jonez"
},
"id": "elroy-id",
"email": "elroy77@example.com",
"displayName": "Elroy Jonez",
"password": "bones",
"remoteIdentities": [
{
"connectorId": "local",