dex/server/config.go
Eric Chiang ac73d3cdf2 *: 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
2016-04-06 11:15:05 -07:00

426 lines
11 KiB
Go

package server
import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net/url"
"os"
"path/filepath"
texttemplate "text/template"
"time"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
"github.com/coreos/pkg/health"
"github.com/go-gorp/gorp"
"github.com/coreos/dex/connector"
"github.com/coreos/dex/db"
"github.com/coreos/dex/email"
sessionmanager "github.com/coreos/dex/session/manager"
"github.com/coreos/dex/user"
useremail "github.com/coreos/dex/user/email"
usermanager "github.com/coreos/dex/user/manager"
)
type ServerConfig struct {
IssuerURL string
IssuerName string
IssuerLogoURL string
TemplateDir string
EmailTemplateDirs []string
EmailFromAddress string
EmailerConfigFile string
StateConfig StateConfigurer
EnableRegistration bool
EnableClientRegistration bool
}
type StateConfigurer interface {
Configure(*Server) error
}
type SingleServerConfig struct {
ClientsFile string
ConnectorsFile string
UsersFile string
}
type MultiServerConfig struct {
KeySecrets [][]byte
DatabaseConfig db.Config
UseOldFormat bool
}
func (cfg *ServerConfig) Server() (*Server, error) {
iu, err := url.Parse(cfg.IssuerURL)
if err != nil {
return nil, err
}
tpl, err := getTemplates(cfg.IssuerName, cfg.IssuerLogoURL, cfg.EnableRegistration, cfg.TemplateDir)
if err != nil {
return nil, err
}
km := key.NewPrivateKeyManager()
srv := Server{
IssuerURL: *iu,
KeyManager: km,
Templates: tpl,
HealthChecks: []health.Checkable{km},
Connectors: []connector.Connector{},
EnableRegistration: cfg.EnableRegistration,
EnableClientRegistration: cfg.EnableClientRegistration,
}
err = cfg.StateConfig.Configure(&srv)
if err != nil {
return nil, err
}
err = setTemplates(&srv, tpl)
if err != nil {
return nil, err
}
err = setEmailer(&srv, cfg.IssuerName, cfg.EmailFromAddress, cfg.EmailerConfigFile, cfg.EmailTemplateDirs)
if err != nil {
return nil, err
}
return &srv, nil
}
func (cfg *SingleServerConfig) Configure(srv *Server) error {
k, err := key.GeneratePrivateKey()
if err != nil {
return err
}
dbMap := db.NewMemDB()
ks := key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(24*time.Hour))
kRepo := key.NewPrivateKeySetRepo()
if err = kRepo.Set(ks); err != nil {
return err
}
clients, err := loadClients(cfg.ClientsFile)
if err != nil {
return fmt.Errorf("unable to read clients from file %s: %v", cfg.ClientsFile, err)
}
ciRepo, err := db.NewClientIdentityRepoFromClients(dbMap, clients)
if err != nil {
return fmt.Errorf("failed to create client identity repo: %v", err)
}
f, err := os.Open(cfg.ConnectorsFile)
if err != nil {
return fmt.Errorf("opening connectors file: %v", err)
}
defer f.Close()
cfgs, err := connector.ReadConfigs(f)
if err != nil {
return fmt.Errorf("decoding connector configs: %v", err)
}
cfgRepo := db.NewConnectorConfigRepo(dbMap)
if err := cfgRepo.Set(cfgs); err != nil {
return fmt.Errorf("failed to set connectors: %v", err)
}
sRepo := db.NewSessionRepo(dbMap)
skRepo := db.NewSessionKeyRepo(dbMap)
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
users, pwis, err := loadUsers(cfg.UsersFile)
if err != nil {
return fmt.Errorf("unable to read users from file: %v", err)
}
userRepo, err := db.NewUserRepoFromUsers(dbMap, users)
if err != nil {
return err
}
pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis)
if err != nil {
return err
}
refTokRepo := db.NewRefreshTokenRepo(dbMap)
txnFactory := db.TransactionFactory(dbMap)
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, txnFactory, usermanager.ManagerOptions{})
srv.ClientIdentityRepo = ciRepo
srv.KeySetRepo = kRepo
srv.ConnectorConfigRepo = cfgRepo
srv.UserRepo = userRepo
srv.UserManager = userManager
srv.PasswordInfoRepo = pwiRepo
srv.SessionManager = sm
srv.RefreshTokenRepo = refTokRepo
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbMap))
return nil
}
// 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 nil, nil, err
}
defer f.Close()
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(r).Decode(&c); err != nil {
return nil, err
}
clients := make([]oidc.ClientIdentity, len(c))
for i, client := range c {
redirectURIs := make([]url.URL, len(client.RedirectURLs))
for j, u := range client.RedirectURLs {
uri, err := url.Parse(u)
if err != nil {
return nil, err
}
redirectURIs[j] = *uri
}
clients[i] = oidc.ClientIdentity{
Credentials: oidc.ClientCredentials{
ID: client.ID,
Secret: client.Secret,
},
Metadata: oidc.ClientMetadata{
RedirectURIs: redirectURIs,
},
}
}
return clients, nil
}
func (cfg *MultiServerConfig) Configure(srv *Server) error {
if len(cfg.KeySecrets) == 0 {
return errors.New("missing key secret")
}
if cfg.DatabaseConfig.DSN == "" {
return errors.New("missing database connection string")
}
dbc, err := db.NewConnection(cfg.DatabaseConfig)
if err != nil {
return fmt.Errorf("unable to initialize database connection: %v", err)
}
if _, ok := dbc.Dialect.(gorp.PostgresDialect); !ok {
return errors.New("only postgres backend supported for multi server configurations")
}
kRepo, err := db.NewPrivateKeySetRepo(dbc, cfg.UseOldFormat, cfg.KeySecrets...)
if err != nil {
return fmt.Errorf("unable to create PrivateKeySetRepo: %v", err)
}
ciRepo := db.NewClientIdentityRepo(dbc)
sRepo := db.NewSessionRepo(dbc)
skRepo := db.NewSessionKeyRepo(dbc)
cfgRepo := db.NewConnectorConfigRepo(dbc)
userRepo := db.NewUserRepo(dbc)
pwiRepo := db.NewPasswordInfoRepo(dbc)
userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, db.TransactionFactory(dbc), usermanager.ManagerOptions{})
refreshTokenRepo := db.NewRefreshTokenRepo(dbc)
sm := sessionmanager.NewSessionManager(sRepo, skRepo)
srv.ClientIdentityRepo = ciRepo
srv.KeySetRepo = kRepo
srv.ConnectorConfigRepo = cfgRepo
srv.UserRepo = userRepo
srv.UserManager = userManager
srv.PasswordInfoRepo = pwiRepo
srv.SessionManager = sm
srv.RefreshTokenRepo = refreshTokenRepo
srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbc))
return nil
}
func getTemplates(issuerName, issuerLogoURL string,
enableRegister bool, dir string) (*template.Template, error) {
tpl := template.New("").Funcs(map[string]interface{}{
"issuerName": func() string {
return issuerName
},
"issuerLogoURL": func() string {
return issuerLogoURL
},
"enableRegister": func() bool {
return enableRegister
},
})
return tpl.ParseGlob(dir + "/*.html")
}
func setTemplates(srv *Server, tpls *template.Template) error {
ltpl, err := findTemplate(LoginPageTemplateName, tpls)
if err != nil {
return err
}
srv.LoginTemplate = ltpl
rtpl, err := findTemplate(RegisterTemplateName, tpls)
if err != nil {
return err
}
srv.RegisterTemplate = rtpl
vtpl, err := findTemplate(VerifyEmailTemplateName, tpls)
if err != nil {
return err
}
srv.VerifyEmailTemplate = vtpl
srtpl, err := findTemplate(SendResetPasswordEmailTemplateName, tpls)
if err != nil {
return err
}
srv.SendResetPasswordEmailTemplate = srtpl
rpwtpl, err := findTemplate(ResetPasswordTemplateName, tpls)
if err != nil {
return err
}
srv.ResetPasswordTemplate = rpwtpl
return nil
}
func setEmailer(srv *Server, issuerName, fromAddress, emailerConfigFile string, emailTemplateDirs []string) error {
cfg, err := email.NewEmailerConfigFromFile(emailerConfigFile)
if err != nil {
return err
}
emailer, err := cfg.Emailer()
if err != nil {
return err
}
getFileNames := func(dir, ext string) ([]string, error) {
fns, err := filepath.Glob(dir + "/*." + ext)
if err != nil {
return nil, err
}
return fns, nil
}
getTextFiles := func(dir string) ([]string, error) {
return getFileNames(dir, "txt")
}
getHTMLFiles := func(dir string) ([]string, error) {
return getFileNames(dir, "html")
}
textTemplates := texttemplate.New("textTemplates")
htmlTemplates := template.New("htmlTemplates")
for _, dir := range emailTemplateDirs {
textFileNames, err := getTextFiles(dir)
if err != nil {
return err
}
if len(textFileNames) != 0 {
textTemplates, err = textTemplates.ParseFiles(textFileNames...)
}
if err != nil {
return err
}
htmlFileNames, err := getHTMLFiles(dir)
if err != nil {
return err
}
if len(htmlFileNames) != 0 {
htmlTemplates, err = htmlTemplates.ParseFiles(htmlFileNames...)
}
if err != nil {
return err
}
}
tMailer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer)
tMailer.SetGlobalContext(map[string]interface{}{
"issuer_name": issuerName,
})
ue := useremail.NewUserEmailer(srv.UserRepo,
srv.PasswordInfoRepo,
srv.KeyManager.Signer,
srv.SessionManager.ValidityWindow,
srv.IssuerURL,
tMailer,
fromAddress,
srv.absURL(httpPathResetPassword),
srv.absURL(httpPathEmailVerify),
srv.absURL(httpPathAcceptInvitation),
)
srv.UserEmailer = ue
return nil
}
func findTemplate(name string, tpls *template.Template) (*template.Template, error) {
tpl := tpls.Lookup(name)
if tpl == nil {
return nil, fmt.Errorf("unable to find template: %q", name)
}
return tpl, nil
}