user_handler.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. package api
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "math/rand"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "golang.org/x/crypto/bcrypt"
  13. "gorm.io/gorm"
  14. "github.com/go-chi/chi"
  15. "github.com/porter-dev/porter/internal/auth/token"
  16. "github.com/porter-dev/porter/internal/forms"
  17. "github.com/porter-dev/porter/internal/models"
  18. "github.com/porter-dev/porter/internal/repository"
  19. )
  20. // Enumeration of user API error codes, represented as int64
  21. const (
  22. ErrUserDecode ErrorCode = iota + 600
  23. ErrUserValidateFields
  24. ErrUserDataRead
  25. )
  26. // HandleCreateUser validates a user form entry, converts the user to a gorm
  27. // model, and saves the user to the database
  28. func (app *App) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
  29. session, err := app.Store.Get(r, app.ServerConf.CookieName)
  30. if err != nil {
  31. app.handleErrorDataRead(err, w)
  32. }
  33. form := &forms.CreateUserForm{}
  34. user, err := app.writeUser(
  35. form,
  36. app.Repo.User.CreateUser,
  37. w,
  38. r,
  39. doesUserExist,
  40. )
  41. if err == nil {
  42. app.Logger.Info().Msgf("New user created: %d", user.ID)
  43. var redirect string
  44. if valR := session.Values["redirect"]; valR != nil {
  45. redirect = session.Values["redirect"].(string)
  46. }
  47. session.Values["authenticated"] = true
  48. session.Values["user_id"] = user.ID
  49. session.Values["email"] = user.Email
  50. session.Values["redirect"] = ""
  51. session.Save(r, w)
  52. w.WriteHeader(http.StatusCreated)
  53. if err := app.sendUser(w, user.ID, user.Email, redirect); err != nil {
  54. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  55. return
  56. }
  57. }
  58. }
  59. // HandleAuthCheck checks whether current session is authenticated and returns user ID if so.
  60. func (app *App) HandleAuthCheck(w http.ResponseWriter, r *http.Request) {
  61. // first, check for token
  62. tok := app.getTokenFromRequest(r)
  63. if tok != nil {
  64. // read the user
  65. user, err := app.Repo.User.ReadUser(tok.IBy)
  66. if err != nil {
  67. http.Error(w, err.Error(), http.StatusInternalServerError)
  68. return
  69. }
  70. if err := app.sendUser(w, tok.IBy, user.Email, ""); err != nil {
  71. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  72. return
  73. }
  74. return
  75. }
  76. session, err := app.Store.Get(r, app.ServerConf.CookieName)
  77. if err != nil {
  78. http.Error(w, err.Error(), http.StatusInternalServerError)
  79. return
  80. }
  81. userID, _ := session.Values["user_id"].(uint)
  82. email, _ := session.Values["email"].(string)
  83. w.WriteHeader(http.StatusOK)
  84. if err := app.sendUser(w, userID, email, ""); err != nil {
  85. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  86. return
  87. }
  88. }
  89. // HandleCLILoginUser verifies that a user is logged in, and generates an access
  90. // token for usage from the CLI
  91. func (app *App) HandleCLILoginUser(w http.ResponseWriter, r *http.Request) {
  92. queryParams, _ := url.ParseQuery(r.URL.RawQuery)
  93. redirect := queryParams["redirect"][0]
  94. session, err := app.Store.Get(r, app.ServerConf.CookieName)
  95. if err != nil {
  96. http.Error(w, err.Error(), http.StatusInternalServerError)
  97. return
  98. }
  99. userID, _ := session.Values["user_id"].(uint)
  100. // generate the token
  101. jwt, err := token.GetTokenForUser(userID)
  102. if err != nil {
  103. app.handleErrorInternal(err, w)
  104. return
  105. }
  106. encoded, err := jwt.EncodeToken(&token.TokenGeneratorConf{
  107. TokenSecret: app.ServerConf.TokenGeneratorSecret,
  108. })
  109. if err != nil {
  110. app.handleErrorInternal(err, w)
  111. return
  112. }
  113. // generate 64 characters long authorization code
  114. const letters = "abcdefghijklmnopqrstuvwxyz123456789"
  115. code := make([]byte, 64)
  116. for i := range code {
  117. code[i] = letters[rand.Intn(len(letters))]
  118. }
  119. expiry := time.Now().Add(30 * time.Second)
  120. // create auth code object and send back authorization code
  121. authCode := &models.AuthCode{
  122. Token: encoded,
  123. AuthorizationCode: string(code),
  124. Expiry: &expiry,
  125. }
  126. authCode, err = app.Repo.AuthCode.CreateAuthCode(authCode)
  127. if err != nil {
  128. app.handleErrorInternal(err, w)
  129. return
  130. }
  131. http.Redirect(w, r, fmt.Sprintf("%s/?code=%s", redirect, url.QueryEscape(authCode.AuthorizationCode)), 302)
  132. }
  133. type ExchangeRequest struct {
  134. AuthorizationCode string `json:"authorization_code"`
  135. }
  136. type ExchangeResponse struct {
  137. Token string `json:"token"`
  138. }
  139. // HandleCLILoginExchangeToken exchanges an authorization code for a token
  140. func (app *App) HandleCLILoginExchangeToken(w http.ResponseWriter, r *http.Request) {
  141. // read the request body and look up the authorization token
  142. req := &ExchangeRequest{}
  143. if err := json.NewDecoder(r.Body).Decode(req); err != nil {
  144. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  145. return
  146. }
  147. authCode, err := app.Repo.AuthCode.ReadAuthCode(req.AuthorizationCode)
  148. if err != nil || authCode.IsExpired() {
  149. http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
  150. return
  151. }
  152. res := &ExchangeResponse{
  153. Token: authCode.Token,
  154. }
  155. w.WriteHeader(http.StatusOK)
  156. if err := json.NewEncoder(w).Encode(res); err != nil {
  157. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  158. return
  159. }
  160. }
  161. // HandleLoginUser checks the request header for cookie and validates the user.
  162. func (app *App) HandleLoginUser(w http.ResponseWriter, r *http.Request) {
  163. session, err := app.Store.Get(r, app.ServerConf.CookieName)
  164. if err != nil {
  165. app.handleErrorDataRead(err, w)
  166. return
  167. }
  168. form := &forms.LoginUserForm{}
  169. // decode from JSON to form value
  170. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  171. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  172. return
  173. }
  174. storedUser, readErr := app.Repo.User.ReadUserByEmail(form.Email)
  175. if readErr != nil {
  176. app.sendExternalError(readErr, http.StatusUnauthorized, HTTPError{
  177. Errors: []string{"email not registered"},
  178. Code: http.StatusUnauthorized,
  179. }, w)
  180. return
  181. }
  182. if err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(form.Password)); err != nil {
  183. app.sendExternalError(readErr, http.StatusUnauthorized, HTTPError{
  184. Errors: []string{"incorrect password"},
  185. Code: http.StatusUnauthorized,
  186. }, w)
  187. return
  188. }
  189. var redirect string
  190. if valR := session.Values["redirect"]; valR != nil {
  191. redirect = session.Values["redirect"].(string)
  192. }
  193. // Set user as authenticated
  194. session.Values["authenticated"] = true
  195. session.Values["user_id"] = storedUser.ID
  196. session.Values["email"] = storedUser.Email
  197. session.Values["redirect"] = ""
  198. if err := session.Save(r, w); err != nil {
  199. app.Logger.Warn().Err(err)
  200. }
  201. w.WriteHeader(http.StatusOK)
  202. if err := app.sendUser(w, storedUser.ID, storedUser.Email, redirect); err != nil {
  203. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  204. return
  205. }
  206. }
  207. // HandleLogoutUser detaches the user from the session
  208. func (app *App) HandleLogoutUser(w http.ResponseWriter, r *http.Request) {
  209. session, err := app.Store.Get(r, app.ServerConf.CookieName)
  210. if err != nil {
  211. app.handleErrorDataRead(err, w)
  212. }
  213. session.Values["authenticated"] = false
  214. session.Values["user_id"] = nil
  215. session.Values["email"] = nil
  216. session.Save(r, w)
  217. w.WriteHeader(http.StatusOK)
  218. }
  219. // HandleReadUser returns an externalized User (models.UserExternal)
  220. // based on an ID
  221. func (app *App) HandleReadUser(w http.ResponseWriter, r *http.Request) {
  222. user, err := app.readUser(w, r)
  223. // error already handled by helper
  224. if err != nil {
  225. return
  226. }
  227. extUser := user.Externalize()
  228. if err := json.NewEncoder(w).Encode(extUser); err != nil {
  229. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  230. return
  231. }
  232. w.WriteHeader(http.StatusOK)
  233. }
  234. // HandleListUserProjects lists all projects belonging to a given user
  235. func (app *App) HandleListUserProjects(w http.ResponseWriter, r *http.Request) {
  236. id, err := strconv.ParseUint(chi.URLParam(r, "user_id"), 0, 64)
  237. if err != nil || id == 0 {
  238. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  239. return
  240. }
  241. projects, err := app.Repo.Project.ListProjectsByUserID(uint(id))
  242. if err != nil {
  243. app.handleErrorRead(err, ErrUserDataRead, w)
  244. }
  245. projectsExt := make([]*models.ProjectExternal, 0)
  246. for _, project := range projects {
  247. projectsExt = append(projectsExt, project.Externalize())
  248. }
  249. w.WriteHeader(http.StatusOK)
  250. if err := json.NewEncoder(w).Encode(projectsExt); err != nil {
  251. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  252. return
  253. }
  254. }
  255. // HandleDeleteUser removes a user after checking that the sent password is correct
  256. func (app *App) HandleDeleteUser(w http.ResponseWriter, r *http.Request) {
  257. id, err := strconv.ParseUint(chi.URLParam(r, "user_id"), 0, 64)
  258. if err != nil || id == 0 {
  259. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  260. return
  261. }
  262. // TODO -- HASH AND VERIFY PASSWORD BEFORE USER DELETION
  263. form := &forms.DeleteUserForm{
  264. ID: uint(id),
  265. }
  266. user, err := app.writeUser(form, app.Repo.User.DeleteUser, w, r)
  267. if err == nil {
  268. app.Logger.Info().Msgf("User deleted: %d", user.ID)
  269. w.WriteHeader(http.StatusNoContent)
  270. }
  271. }
  272. // ------------------------ User handler helper functions ------------------------ //
  273. // writeUser will take a POST or PUT request to the /api/users endpoint and decode
  274. // the request into a forms.WriteUserForm model, convert it to a models.User, and
  275. // write to the database.
  276. func (app *App) writeUser(
  277. form forms.WriteUserForm,
  278. dbWrite repository.WriteUser,
  279. w http.ResponseWriter,
  280. r *http.Request,
  281. validators ...func(repo *repository.Repository, user *models.User) *HTTPError,
  282. ) (*models.User, error) {
  283. // decode from JSON to form value
  284. if err := json.NewDecoder(r.Body).Decode(form); err != nil {
  285. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  286. return nil, err
  287. }
  288. // validate the form
  289. if err := app.validator.Struct(form); err != nil {
  290. app.handleErrorFormValidation(err, ErrUserValidateFields, w)
  291. return nil, err
  292. }
  293. // convert the form to a user model -- WriteUserForm must implement ToUser
  294. userModel, err := form.ToUser(app.Repo.User)
  295. if err != nil {
  296. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  297. return nil, err
  298. }
  299. // Check any additional validators for any semantic errors
  300. // We have completed all syntax checks, so these will be sent
  301. // with http.StatusUnprocessableEntity (422), unless this is
  302. // an internal server error
  303. for _, validator := range validators {
  304. err := validator(app.Repo, userModel)
  305. if err != nil {
  306. goErr := errors.New(strings.Join(err.Errors, ", "))
  307. if err.Code == 500 {
  308. app.sendExternalError(
  309. goErr,
  310. http.StatusInternalServerError,
  311. *err,
  312. w,
  313. )
  314. } else {
  315. app.sendExternalError(
  316. goErr,
  317. http.StatusUnprocessableEntity,
  318. *err,
  319. w,
  320. )
  321. }
  322. return nil, goErr
  323. }
  324. }
  325. // handle write to the database
  326. user, err := dbWrite(userModel)
  327. if err != nil {
  328. app.handleErrorDataWrite(err, w)
  329. return nil, err
  330. }
  331. return user, nil
  332. }
  333. func (app *App) readUser(w http.ResponseWriter, r *http.Request) (*models.User, error) {
  334. id, err := strconv.ParseUint(chi.URLParam(r, "user_id"), 0, 64)
  335. if err != nil || id == 0 {
  336. app.handleErrorFormDecoding(err, ErrUserDecode, w)
  337. return nil, err
  338. }
  339. user, err := app.Repo.User.ReadUser(uint(id))
  340. if err != nil {
  341. app.handleErrorRead(err, ErrUserDataRead, w)
  342. return nil, err
  343. }
  344. return user, nil
  345. }
  346. func doesUserExist(repo *repository.Repository, user *models.User) *HTTPError {
  347. user, err := repo.User.ReadUserByEmail(user.Email)
  348. if user != nil && err == nil {
  349. return &HTTPError{
  350. Code: ErrUserValidateFields,
  351. Errors: []string{
  352. "email already taken",
  353. },
  354. }
  355. }
  356. if err != gorm.ErrRecordNotFound {
  357. return &ErrorDataRead
  358. }
  359. return nil
  360. }
  361. type SendUserExt struct {
  362. ID uint `json:"id"`
  363. Email string `json:"email"`
  364. Redirect string `json:"redirect,omitempty"`
  365. }
  366. func (app *App) sendUser(w http.ResponseWriter, userID uint, email, redirect string) error {
  367. resUser := &SendUserExt{
  368. ID: userID,
  369. Email: email,
  370. Redirect: redirect,
  371. }
  372. if err := json.NewEncoder(w).Encode(resUser); err != nil {
  373. return err
  374. }
  375. return nil
  376. }