forked from mystiq/dex
This change solves the User's API problem when you want to create an user that its email hasn't been verified yet but it exist. At now, you can resend invitation email using endpoint /users/{id}/resend-invitation Fixes #184
305 lines
8 KiB
Go
305 lines
8 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/coreos/dex/client"
|
|
"github.com/coreos/dex/pkg/log"
|
|
schema "github.com/coreos/dex/schema/workerschema"
|
|
"github.com/coreos/dex/user"
|
|
"github.com/coreos/dex/user/manager"
|
|
)
|
|
|
|
var (
|
|
errorMap = map[error]Error{
|
|
user.ErrorNotFound: ErrorResourceNotFound,
|
|
user.ErrorDuplicateEmail: ErrorDuplicateEmail,
|
|
user.ErrorInvalidEmail: ErrorInvalidEmail,
|
|
client.ErrorNotFound: ErrorInvalidClient,
|
|
}
|
|
|
|
ErrorInvalidEmail = newError("invalid_email", "invalid email.", http.StatusBadRequest)
|
|
ErrorVerifiedEmail = newError("verified_email", "Email already verified.", http.StatusBadRequest)
|
|
|
|
ErrorInvalidClient = newError("invalid_client", "invalid email.", http.StatusBadRequest)
|
|
|
|
ErrorDuplicateEmail = newError("duplicate_email", "Email already in use.", http.StatusBadRequest)
|
|
ErrorResourceNotFound = newError("resource_not_found", "Resource could not be found.", http.StatusNotFound)
|
|
|
|
ErrorUnauthorized = newError("unauthorized", "Necessary credentials not provided.", http.StatusUnauthorized)
|
|
ErrorForbidden = newError("forbidden", "The given user and client are not authorized to make this request.", http.StatusForbidden)
|
|
|
|
ErrorMaxResultsTooHigh = newError("max_results_too_high", fmt.Sprintf("The max number of results per page is %d", maxUsersPerPage), http.StatusBadRequest)
|
|
|
|
ErrorInvalidRedirectURL = newError("invalid_redirect_url", "The provided redirect URL is invalid for the given client", http.StatusBadRequest)
|
|
)
|
|
|
|
const (
|
|
maxUsersPerPage = 100
|
|
)
|
|
|
|
func internalError(internal error) Error {
|
|
return Error{
|
|
Code: http.StatusInternalServerError,
|
|
Type: "server_error",
|
|
Desc: "",
|
|
Internal: internal,
|
|
}
|
|
}
|
|
|
|
func newError(typ string, desc string, code int) Error {
|
|
return Error{
|
|
Code: code,
|
|
Type: typ,
|
|
Desc: desc,
|
|
}
|
|
}
|
|
|
|
// Error is the error type returned by AdminAPI methods.
|
|
type Error struct {
|
|
Type string
|
|
|
|
// The HTTP Code to return for this type of error.
|
|
Code int
|
|
|
|
Desc string
|
|
|
|
// The underlying error - not to be consumed by external users.
|
|
Internal error
|
|
}
|
|
|
|
func (e Error) Error() string {
|
|
return fmt.Sprintf("%v: Desc: %v Internal: %v", e.Type, e.Desc, e.Internal)
|
|
}
|
|
|
|
// UsersAPI is the user management API for Dex administrators.
|
|
|
|
// All calls take a Creds object with the ClientID of the calling app and the
|
|
// calling User. It is assumed that the clientID has already validated as an
|
|
// admin app before calling.
|
|
type UsersAPI struct {
|
|
manager *manager.UserManager
|
|
localConnectorID string
|
|
clientIdentityRepo client.ClientIdentityRepo
|
|
emailer Emailer
|
|
}
|
|
|
|
type Emailer interface {
|
|
SendInviteEmail(string, url.URL, string) (*url.URL, error)
|
|
}
|
|
|
|
type Creds struct {
|
|
ClientID string
|
|
User user.User
|
|
}
|
|
|
|
func NewUsersAPI(manager *manager.UserManager, cir client.ClientIdentityRepo, emailer Emailer, localConnectorID string) *UsersAPI {
|
|
return &UsersAPI{
|
|
manager: manager,
|
|
clientIdentityRepo: cir,
|
|
localConnectorID: localConnectorID,
|
|
emailer: emailer,
|
|
}
|
|
}
|
|
|
|
func (u *UsersAPI) GetUser(creds Creds, id string) (schema.User, error) {
|
|
log.Infof("userAPI: GetUser")
|
|
|
|
if !u.Authorize(creds) {
|
|
return schema.User{}, ErrorUnauthorized
|
|
}
|
|
|
|
usr, err := u.manager.Get(id)
|
|
|
|
if err != nil {
|
|
return schema.User{}, mapError(err)
|
|
}
|
|
|
|
return userToSchemaUser(usr), nil
|
|
}
|
|
|
|
func (u *UsersAPI) DisableUser(creds Creds, userID string, disable bool) (schema.UserDisableResponse, error) {
|
|
log.Infof("userAPI: DisableUser")
|
|
if !u.Authorize(creds) {
|
|
return schema.UserDisableResponse{}, ErrorUnauthorized
|
|
}
|
|
|
|
if err := u.manager.Disable(userID, disable); err != nil {
|
|
return schema.UserDisableResponse{}, mapError(err)
|
|
}
|
|
|
|
return schema.UserDisableResponse{
|
|
Ok: true,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (schema.UserCreateResponse, error) {
|
|
log.Infof("userAPI: CreateUser")
|
|
if !u.Authorize(creds) {
|
|
return schema.UserCreateResponse{}, ErrorUnauthorized
|
|
}
|
|
|
|
hash, err := generateTempHash()
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
metadata, err := u.clientIdentityRepo.Metadata(creds.ClientID)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, ErrorInvalidRedirectURL
|
|
}
|
|
|
|
id, err := u.manager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
userUser, err := u.manager.Get(id)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
usr = userToSchemaUser(userUser)
|
|
|
|
url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID)
|
|
|
|
// An email is sent only if we don't get a link and there's no error.
|
|
emailSent := err == nil && url == nil
|
|
|
|
var resetLink string
|
|
if url != nil {
|
|
resetLink = url.String()
|
|
}
|
|
|
|
return schema.UserCreateResponse{
|
|
User: &usr,
|
|
EmailSent: emailSent,
|
|
ResetPasswordLink: resetLink,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UsersAPI) ResendEmailInvitation(creds Creds, userID string, redirURL url.URL) (schema.ResendEmailInvitationResponse, error) {
|
|
log.Infof("userAPI: ResendEmailInvitation")
|
|
if !u.Authorize(creds) {
|
|
return schema.ResendEmailInvitationResponse{}, ErrorUnauthorized
|
|
}
|
|
|
|
metadata, err := u.clientIdentityRepo.Metadata(creds.ClientID)
|
|
if err != nil {
|
|
return schema.ResendEmailInvitationResponse{}, mapError(err)
|
|
}
|
|
|
|
validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURIs)
|
|
if err != nil {
|
|
return schema.ResendEmailInvitationResponse{}, ErrorInvalidRedirectURL
|
|
}
|
|
|
|
// Retrieve user to check if it's already created
|
|
userUser, err := u.manager.Get(userID)
|
|
if err != nil {
|
|
return schema.ResendEmailInvitationResponse{}, mapError(err)
|
|
}
|
|
|
|
// Check if email is verified
|
|
if userUser.EmailVerified {
|
|
return schema.ResendEmailInvitationResponse{}, ErrorVerifiedEmail
|
|
}
|
|
|
|
url, err := u.emailer.SendInviteEmail(userUser.Email, validRedirURL, creds.ClientID)
|
|
|
|
// An email is sent only if we don't get a link and there's no error.
|
|
emailSent := err == nil && url == nil
|
|
|
|
// If email is not sent a reset link will be generated
|
|
var resetLink string
|
|
if url != nil {
|
|
resetLink = url.String()
|
|
}
|
|
|
|
return schema.ResendEmailInvitationResponse{
|
|
EmailSent: emailSent,
|
|
ResetPasswordLink: resetLink,
|
|
}, nil
|
|
}
|
|
|
|
func (u *UsersAPI) ListUsers(creds Creds, maxResults int, nextPageToken string) ([]*schema.User, string, error) {
|
|
log.Infof("userAPI: ListUsers")
|
|
|
|
if !u.Authorize(creds) {
|
|
return nil, "", ErrorUnauthorized
|
|
}
|
|
|
|
if maxResults > maxUsersPerPage {
|
|
return nil, "", ErrorMaxResultsTooHigh
|
|
}
|
|
|
|
users, tok, err := u.manager.List(user.UserFilter{}, maxResults, nextPageToken)
|
|
if err != nil {
|
|
return nil, "", mapError(err)
|
|
}
|
|
|
|
list := []*schema.User{}
|
|
for _, usr := range users {
|
|
schemaUsr := userToSchemaUser(usr)
|
|
list = append(list, &schemaUsr)
|
|
}
|
|
|
|
return list, tok, nil
|
|
}
|
|
|
|
func (u *UsersAPI) Authorize(creds Creds) bool {
|
|
return creds.User.Admin && !creds.User.Disabled
|
|
}
|
|
|
|
func userToSchemaUser(usr user.User) schema.User {
|
|
return schema.User{
|
|
Id: usr.ID,
|
|
Email: usr.Email,
|
|
EmailVerified: usr.EmailVerified,
|
|
DisplayName: usr.DisplayName,
|
|
Admin: usr.Admin,
|
|
Disabled: usr.Disabled,
|
|
CreatedAt: usr.CreatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func schemaUserToUser(usr schema.User) user.User {
|
|
return user.User{
|
|
ID: usr.Id,
|
|
Email: usr.Email,
|
|
EmailVerified: usr.EmailVerified,
|
|
DisplayName: usr.DisplayName,
|
|
Admin: usr.Admin,
|
|
Disabled: usr.Disabled,
|
|
}
|
|
}
|
|
|
|
func mapError(e error) error {
|
|
if mapped, ok := errorMap[e]; ok {
|
|
return mapped
|
|
}
|
|
return internalError(e)
|
|
}
|
|
|
|
func generateTempHash() (string, error) {
|
|
b := make([]byte, 32)
|
|
n, err := rand.Read(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if n != 32 {
|
|
return "", errors.New("unable to read enough random bytes")
|
|
}
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|