358 lines
10 KiB
Go
358 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/coreos/dex/client"
|
|
clientmanager "github.com/coreos/dex/client/manager"
|
|
"github.com/coreos/dex/pkg/log"
|
|
"github.com/coreos/dex/refresh"
|
|
schema "github.com/coreos/dex/schema/workerschema"
|
|
"github.com/coreos/dex/user"
|
|
usermanager "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.StatusConflict)
|
|
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 {
|
|
userManager *usermanager.UserManager
|
|
localConnectorID string
|
|
clientManager *clientmanager.ClientManager
|
|
refreshRepo refresh.RefreshTokenRepo
|
|
emailer Emailer
|
|
}
|
|
|
|
type Emailer interface {
|
|
SendInviteEmail(string, url.URL, string) (*url.URL, error)
|
|
}
|
|
|
|
type Creds struct {
|
|
// IDTokens can be issued for multiple clients.
|
|
ClientIDs []string
|
|
User user.User
|
|
}
|
|
|
|
// TODO(ericchiang): Don't pass a dbMap. See #385.
|
|
func NewUsersAPI(userManager *usermanager.UserManager, clientManager *clientmanager.ClientManager, refreshRepo refresh.RefreshTokenRepo, emailer Emailer, localConnectorID string) *UsersAPI {
|
|
return &UsersAPI{
|
|
userManager: userManager,
|
|
refreshRepo: refreshRepo,
|
|
clientManager: clientManager,
|
|
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.userManager.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.userManager.Disable(userID, disable); err != nil {
|
|
return schema.UserDisableResponse{}, mapError(err)
|
|
}
|
|
|
|
return schema.UserDisableResponse{
|
|
Ok: true,
|
|
}, nil
|
|
}
|
|
|
|
// validRedirectURL finds the first client for which the redirect URL is valid. If found it returns the client_id of the client.
|
|
func validRedirectURL(clientManager *clientmanager.ClientManager, redirectURL url.URL, clientIDs []string) (string, error) {
|
|
// Find the first client with a valid redirectURL.
|
|
for _, clientID := range clientIDs {
|
|
metadata, err := clientManager.Metadata(clientID)
|
|
if err != nil {
|
|
return "", mapError(err)
|
|
}
|
|
|
|
if _, err := client.ValidRedirectURL(&redirectURL, metadata.RedirectURIs); err == nil {
|
|
return clientID, nil
|
|
}
|
|
}
|
|
return "", ErrorInvalidRedirectURL
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
clientID, err := validRedirectURL(u.clientManager, redirURL, creds.ClientIDs)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, err
|
|
}
|
|
|
|
id, err := u.userManager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
userUser, err := u.userManager.Get(id)
|
|
if err != nil {
|
|
return schema.UserCreateResponse{}, mapError(err)
|
|
}
|
|
|
|
usr = userToSchemaUser(userUser)
|
|
|
|
url, err := u.emailer.SendInviteEmail(usr.Email, redirURL, 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
|
|
}
|
|
|
|
clientID, err := validRedirectURL(u.clientManager, redirURL, creds.ClientIDs)
|
|
if err != nil {
|
|
return schema.ResendEmailInvitationResponse{}, err
|
|
}
|
|
|
|
// Retrieve user to check if it's already created
|
|
userUser, err := u.userManager.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, redirURL, 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.userManager.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
|
|
}
|
|
|
|
// ListClientsWithRefreshTokens returns all clients issued refresh tokens
|
|
// for the authenticated user.
|
|
func (u *UsersAPI) ListClientsWithRefreshTokens(creds Creds, userID string) ([]*schema.RefreshClient, error) {
|
|
// Users must either be an admin or be requesting data associated with their own account.
|
|
if !creds.User.Admin && (creds.User.ID != userID) {
|
|
return nil, ErrorUnauthorized
|
|
}
|
|
clientIdentities, err := u.refreshRepo.ClientsWithRefreshTokens(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clients := make([]*schema.RefreshClient, len(clientIdentities))
|
|
|
|
urlToString := func(u *url.URL) string {
|
|
if u == nil {
|
|
return ""
|
|
}
|
|
return u.String()
|
|
}
|
|
|
|
for i, identity := range clientIdentities {
|
|
clients[i] = &schema.RefreshClient{
|
|
ClientID: identity.Credentials.ID,
|
|
ClientName: identity.Metadata.ClientName,
|
|
ClientURI: urlToString(identity.Metadata.ClientURI),
|
|
LogoURI: urlToString(identity.Metadata.LogoURI),
|
|
}
|
|
}
|
|
return clients, nil
|
|
}
|
|
|
|
// RevokeClient revokes all refresh tokens issued to this client for the
|
|
// authenticiated user.
|
|
func (u *UsersAPI) RevokeRefreshTokensForClient(creds Creds, userID, clientID string) error {
|
|
// Users must either be an admin or be requesting data associated with their own account.
|
|
if !creds.User.Admin && (creds.User.ID != userID) {
|
|
return ErrorUnauthorized
|
|
}
|
|
return u.refreshRepo.RevokeTokensForClient(userID, clientID)
|
|
}
|
|
|
|
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
|
|
}
|