forked from mystiq/dex
26508c6bab
When automatically registering a user from an IP that provides a `DisplayName`, set it on the created user so that JWT we create contain a meaningful `name` field.
376 lines
8.2 KiB
Go
376 lines
8.2 KiB
Go
package manager
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
|
|
"github.com/jonboulle/clockwork"
|
|
|
|
"github.com/coreos/dex/connector"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/dex/repo"
|
|
"github.com/coreos/dex/user"
|
|
)
|
|
|
|
var (
|
|
ErrorEmailAlreadyVerified = errors.New("email already verified")
|
|
ErrorPasswordAlreadyChanged = errors.New("password has already been changed")
|
|
)
|
|
|
|
// Manager performs user-related "business-logic" functions on user and related objects.
|
|
// This is in contrast to the Repos which perform little more than CRUD operations.
|
|
type UserManager struct {
|
|
Clock clockwork.Clock
|
|
|
|
userRepo user.UserRepo
|
|
pwRepo user.PasswordInfoRepo
|
|
connCfgRepo connector.ConnectorConfigRepo
|
|
begin repo.TransactionFactory
|
|
userIDGenerator user.UserIDGenerator
|
|
}
|
|
|
|
type ManagerOptions struct {
|
|
// This is empty right now but will soon contain configuration information
|
|
// such as passowrd length, name length, password expiration time and other
|
|
// variable policies
|
|
}
|
|
|
|
func NewUserManager(userRepo user.UserRepo, pwRepo user.PasswordInfoRepo, connCfgRepo connector.ConnectorConfigRepo, txnFactory repo.TransactionFactory, options ManagerOptions) *UserManager {
|
|
return &UserManager{
|
|
Clock: clockwork.NewRealClock(),
|
|
|
|
userRepo: userRepo,
|
|
pwRepo: pwRepo,
|
|
connCfgRepo: connCfgRepo,
|
|
begin: txnFactory,
|
|
userIDGenerator: user.DefaultUserIDGenerator,
|
|
}
|
|
}
|
|
|
|
func (m *UserManager) Get(id string) (user.User, error) {
|
|
return m.userRepo.Get(nil, id)
|
|
}
|
|
|
|
func (m *UserManager) List(filter user.UserFilter, maxResults int, nextPageToken string) ([]user.User, string, error) {
|
|
return m.userRepo.List(nil, filter, maxResults, nextPageToken)
|
|
}
|
|
|
|
// CreateUser creates a new user with the given hashedPassword; the connID should be the ID of the local connector.
|
|
// The userID of the created user is returned as the first argument.
|
|
func (m *UserManager) CreateUser(usr user.User, hashedPassword user.Password, connID string) (string, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
insertedUser, err := m.insertNewUser(tx, usr.Email, usr.EmailVerified)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
usr.ID = insertedUser.ID
|
|
usr.CreatedAt = insertedUser.CreatedAt
|
|
err = m.userRepo.Update(tx, usr)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
rid := user.RemoteIdentity{
|
|
ConnectorID: connID,
|
|
ID: usr.ID,
|
|
}
|
|
if err := m.addRemoteIdentity(tx, usr.ID, rid); err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
pwi := user.PasswordInfo{
|
|
UserID: usr.ID,
|
|
Password: hashedPassword,
|
|
}
|
|
err = m.pwRepo.Create(tx, pwi)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
return usr.ID, nil
|
|
}
|
|
|
|
func (m *UserManager) Disable(userID string, disabled bool) error {
|
|
tx, err := m.begin()
|
|
|
|
if err = m.userRepo.Disable(tx, userID, disabled); err != nil {
|
|
rollback(tx)
|
|
return err
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
rollback(tx)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *UserManager) SetDisplayName(usr user.User, displayName string) error {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rollback(tx)
|
|
|
|
usr.DisplayName = displayName
|
|
if err = m.userRepo.Update(tx, usr); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterWithRemoteIdentity creates new user and attaches the given remote identity.
|
|
func (m *UserManager) RegisterWithRemoteIdentity(email string, emailVerified bool, rid user.RemoteIdentity) (string, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err = m.userRepo.GetByRemoteIdentity(tx, rid); err == nil {
|
|
rollback(tx)
|
|
return "", user.ErrorDuplicateRemoteIdentity
|
|
}
|
|
if err != user.ErrorNotFound {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
usr, err := m.insertNewUser(tx, email, emailVerified)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
if err := m.addRemoteIdentity(tx, usr.ID, rid); err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
return usr.ID, nil
|
|
}
|
|
|
|
// RegisterWithPassword creates a new user with the given name and password.
|
|
// connID is the connector ID of the ConnectorLocal connector.
|
|
func (m *UserManager) RegisterWithPassword(email, plaintext, connID string) (string, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !user.ValidPassword(plaintext) {
|
|
rollback(tx)
|
|
return "", user.ErrorInvalidPassword
|
|
}
|
|
|
|
usr, err := m.insertNewUser(tx, email, false)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
rid := user.RemoteIdentity{
|
|
ConnectorID: connID,
|
|
ID: usr.ID,
|
|
}
|
|
if err := m.addRemoteIdentity(tx, usr.ID, rid); err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
password, err := user.NewPasswordFromPlaintext(plaintext)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
pwi := user.PasswordInfo{
|
|
UserID: usr.ID,
|
|
Password: password,
|
|
}
|
|
|
|
err = m.pwRepo.Create(tx, pwi)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
rollback(tx)
|
|
return "", err
|
|
}
|
|
return usr.ID, nil
|
|
}
|
|
|
|
type EmailVerifiable interface {
|
|
UserID() string
|
|
Email() string
|
|
Callback() *url.URL
|
|
}
|
|
|
|
// VerifyEmail sets EmailVerified to true for the user for the given EmailVerification.
|
|
// The email in the EmailVerification must match the User's email in the
|
|
// repository, and it must not already be verified.
|
|
// This function expects that ParseAndVerifyEmailVerificationToken was used to
|
|
// create it, ensuring that the token was signed and that the JWT was not
|
|
// expired.
|
|
// The callback url (i.e. where to send the user after the verification) is returned.
|
|
func (m *UserManager) VerifyEmail(ev EmailVerifiable) (*url.URL, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
usr, err := m.userRepo.GetByEmail(tx, ev.Email())
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
|
|
if usr.ID != ev.UserID() {
|
|
rollback(tx)
|
|
return nil, user.ErrorNotFound
|
|
}
|
|
|
|
if usr.EmailVerified {
|
|
rollback(tx)
|
|
return nil, ErrorEmailAlreadyVerified
|
|
}
|
|
|
|
usr.EmailVerified = true
|
|
|
|
err = m.userRepo.Update(tx, usr)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
return ev.Callback(), nil
|
|
}
|
|
|
|
type PasswordChangeable interface {
|
|
UserID() string
|
|
Password() user.Password
|
|
Callback() *url.URL
|
|
}
|
|
|
|
func (m *UserManager) ChangePassword(pwr PasswordChangeable, plaintext string) (*url.URL, error) {
|
|
tx, err := m.begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !user.ValidPassword(plaintext) {
|
|
rollback(tx)
|
|
return nil, user.ErrorInvalidPassword
|
|
}
|
|
|
|
pwi, err := m.pwRepo.Get(tx, pwr.UserID())
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
|
|
if string(pwi.Password) != string(pwr.Password()) {
|
|
rollback(tx)
|
|
return nil, ErrorPasswordAlreadyChanged
|
|
}
|
|
|
|
newPass, err := user.NewPasswordFromPlaintext(plaintext)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
|
|
pwi.Password = newPass
|
|
err = m.pwRepo.Update(tx, pwi)
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
rollback(tx)
|
|
return nil, err
|
|
}
|
|
return pwr.Callback(), nil
|
|
}
|
|
|
|
func (m *UserManager) insertNewUser(tx repo.Transaction, email string, emailVerified bool) (user.User, error) {
|
|
if !user.ValidEmail(email) {
|
|
return user.User{}, user.ErrorInvalidEmail
|
|
}
|
|
|
|
var err error
|
|
if _, err = m.userRepo.GetByEmail(tx, email); err == nil {
|
|
return user.User{}, user.ErrorDuplicateEmail
|
|
}
|
|
if err != user.ErrorNotFound {
|
|
return user.User{}, err
|
|
}
|
|
|
|
userID, err := m.userIDGenerator()
|
|
if err != nil {
|
|
return user.User{}, err
|
|
}
|
|
|
|
usr := user.User{
|
|
ID: userID,
|
|
Email: email,
|
|
EmailVerified: emailVerified,
|
|
CreatedAt: m.Clock.Now(),
|
|
}
|
|
|
|
err = m.userRepo.Create(tx, usr)
|
|
if err != nil {
|
|
return user.User{}, err
|
|
}
|
|
return usr, nil
|
|
}
|
|
|
|
func (m *UserManager) addRemoteIdentity(tx repo.Transaction, userID string, rid user.RemoteIdentity) error {
|
|
if _, err := m.connCfgRepo.GetConnectorByID(tx, rid.ConnectorID); err != nil {
|
|
return err
|
|
}
|
|
if err := m.userRepo.AddRemoteIdentity(tx, userID, rid); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rollback(tx repo.Transaction) {
|
|
err := tx.Rollback()
|
|
if err != nil {
|
|
log.Errorf("unable to rollback: %v", err)
|
|
}
|
|
}
|