login.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2017 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. Farhaan Bukhsh <farhaan.bukhsh@gmail.com>
  7. """
  8. from __future__ import unicode_literals
  9. import datetime
  10. import logging
  11. import flask
  12. import six
  13. from sqlalchemy.exc import SQLAlchemyError
  14. from six.moves.urllib.parse import urljoin
  15. import pagure.login_forms as forms
  16. import pagure.lib.login
  17. import pagure.lib.model as model
  18. import pagure.lib.notify
  19. import pagure.lib.query
  20. from pagure.utils import login_required
  21. from pagure.lib.login import generate_hashed_value, check_password
  22. from pagure.ui import UI_NS
  23. _log = logging.getLogger(__name__)
  24. @UI_NS.route("/user/new/", methods=["GET", "POST"])
  25. @UI_NS.route("/user/new", methods=["GET", "POST"])
  26. def new_user():
  27. """ Create a new user.
  28. """
  29. form = forms.NewUserForm()
  30. if form.validate_on_submit():
  31. username = form.user.data
  32. if pagure.lib.query.search_user(flask.g.session, username=username):
  33. flask.flash("Username already taken.", "error")
  34. return flask.redirect(flask.request.url)
  35. email = form.email_address.data
  36. if pagure.lib.query.search_user(flask.g.session, email=email):
  37. flask.flash("Email address already taken.", "error")
  38. return flask.redirect(flask.request.url)
  39. form.password.data = generate_hashed_value(form.password.data)
  40. token = pagure.lib.login.id_generator(40)
  41. user = model.User()
  42. user.token = token
  43. form.populate_obj(obj=user)
  44. user.default_email = form.email_address.data
  45. flask.g.session.add(user)
  46. flask.g.session.flush()
  47. try:
  48. pagure.lib.query.add_email_to_user(
  49. flask.g.session, user, form.email_address.data
  50. )
  51. flask.g.session.commit()
  52. send_confirmation_email(user)
  53. flask.flash(
  54. "User created, please check your email to activate the "
  55. "account"
  56. )
  57. except pagure.exceptions.PagureException as err:
  58. flask.flash(str(err), "error")
  59. _log.exception(err)
  60. except SQLAlchemyError: # pragma: no cover
  61. flask.g.session.rollback()
  62. flask.flash("Could not create user.")
  63. _log.exception("Could not create user.")
  64. return flask.redirect(flask.url_for("auth_login"))
  65. return flask.render_template("login/user_new.html", form=form)
  66. @UI_NS.route("/dologin", methods=["POST"])
  67. def do_login():
  68. """ Log in the user.
  69. """
  70. logout()
  71. form = forms.LoginForm()
  72. next_url = flask.request.form.get("next_url")
  73. if not next_url or next_url == "None":
  74. next_url = flask.url_for("ui_ns.index")
  75. if form.validate_on_submit():
  76. username = form.username.data
  77. user_obj = pagure.lib.query.search_user(
  78. flask.g.session, username=username
  79. )
  80. if not user_obj:
  81. flask.flash("Username or password invalid.", "error")
  82. return flask.redirect(flask.url_for("auth_login"))
  83. try:
  84. password_checks = check_password(
  85. form.password.data,
  86. user_obj.password,
  87. seed=pagure.config.config.get("PASSWORD_SEED", None),
  88. )
  89. except pagure.exceptions.PagureException as err:
  90. _log.exception(err)
  91. flask.flash("Username or password of invalid format.", "error")
  92. return flask.redirect(flask.url_for("auth_login"))
  93. if not password_checks:
  94. flask.flash("Username or password invalid.", "error")
  95. return flask.redirect(flask.url_for("auth_login"))
  96. elif user_obj.token:
  97. flask.flash(
  98. "Invalid user, did you confirm the creation with the url "
  99. "provided by email?",
  100. "error",
  101. )
  102. return flask.redirect(flask.url_for("auth_login"))
  103. else:
  104. password = user_obj.password
  105. if not isinstance(password, six.text_type):
  106. password = password.decode("utf-8")
  107. if not password.startswith("$2$"):
  108. user_obj.password = generate_hashed_value(form.password.data)
  109. flask.g.session.add(user_obj)
  110. flask.g.session.flush()
  111. visit_key = pagure.lib.login.id_generator(40)
  112. now = datetime.datetime.utcnow()
  113. expiry = now + datetime.timedelta(days=30)
  114. session = model.PagureUserVisit(
  115. user_id=user_obj.id,
  116. user_ip=flask.request.remote_addr,
  117. visit_key=visit_key,
  118. expiry=expiry,
  119. )
  120. flask.g.session.add(session)
  121. try:
  122. flask.g.session.commit()
  123. flask.g.fas_user = user_obj
  124. flask.g.fas_session_id = visit_key
  125. flask.g.fas_user.login_time = now
  126. flask.flash("Welcome %s" % user_obj.username)
  127. except SQLAlchemyError as err: # pragma: no cover
  128. flask.flash(
  129. "Could not set the session in the db, "
  130. "please report this error to an admin",
  131. "error",
  132. )
  133. _log.exception(err)
  134. return flask.redirect(next_url)
  135. else:
  136. flask.flash("Insufficient information provided", "error")
  137. return flask.redirect(flask.url_for("auth_login"))
  138. @UI_NS.route("/confirm/<token>/")
  139. @UI_NS.route("/confirm/<token>")
  140. def confirm_user(token):
  141. """ Confirm a user account.
  142. """
  143. user_obj = pagure.lib.query.search_user(flask.g.session, token=token)
  144. if not user_obj:
  145. flask.flash("No user associated with this token.", "error")
  146. else:
  147. user_obj.token = None
  148. flask.g.session.add(user_obj)
  149. try:
  150. flask.g.session.commit()
  151. flask.flash("Email confirmed, account activated")
  152. return flask.redirect(flask.url_for("auth_login"))
  153. except SQLAlchemyError as err: # pragma: no cover
  154. flask.flash(
  155. "Could not set the account as active in the db, "
  156. "please report this error to an admin",
  157. "error",
  158. )
  159. _log.exception(err)
  160. return flask.redirect(flask.url_for("ui_ns.index"))
  161. @UI_NS.route("/password/lost/", methods=["GET", "POST"])
  162. @UI_NS.route("/password/lost", methods=["GET", "POST"])
  163. def lost_password():
  164. """ Method to allow a user to change his/her password assuming the email
  165. is not compromised.
  166. """
  167. form = forms.LostPasswordForm()
  168. if form.validate_on_submit():
  169. username = form.username.data
  170. user_obj = pagure.lib.query.search_user(
  171. flask.g.session, username=username
  172. )
  173. if not user_obj:
  174. flask.flash("Username invalid.", "error")
  175. return flask.redirect(flask.url_for("auth_login"))
  176. elif user_obj.token:
  177. current_time = datetime.datetime.utcnow()
  178. invalid_period = user_obj.updated_on + datetime.timedelta(
  179. minutes=3
  180. )
  181. if current_time < invalid_period:
  182. flask.flash(
  183. "An email was sent to you less than 3 minutes ago, "
  184. "did you check your spam folder? Otherwise, "
  185. "try again after some time.",
  186. "error",
  187. )
  188. return flask.redirect(flask.url_for("auth_login"))
  189. token = pagure.lib.login.id_generator(40)
  190. user_obj.token = token
  191. flask.g.session.add(user_obj)
  192. try:
  193. flask.g.session.commit()
  194. send_lostpassword_email(user_obj)
  195. flask.flash("Check your email to finish changing your password")
  196. except SQLAlchemyError: # pragma: no cover
  197. flask.g.session.rollback()
  198. flask.flash(
  199. "Could not set the token allowing changing a password.",
  200. "error",
  201. )
  202. _log.exception("Password lost change - Error setting token.")
  203. return flask.redirect(flask.url_for("auth_login"))
  204. return flask.render_template("login/password_change.html", form=form)
  205. @UI_NS.route("/password/reset/<token>/", methods=["GET", "POST"])
  206. @UI_NS.route("/password/reset/<token>", methods=["GET", "POST"])
  207. def reset_password(token):
  208. """ Method to allow a user to reset his/her password.
  209. """
  210. form = forms.ResetPasswordForm()
  211. user_obj = pagure.lib.query.search_user(flask.g.session, token=token)
  212. if not user_obj:
  213. flask.flash("No user associated with this token.", "error")
  214. return flask.redirect(flask.url_for("auth_login"))
  215. elif not user_obj.token:
  216. flask.flash(
  217. "Invalid user, this user never asked for a password change",
  218. "error",
  219. )
  220. return flask.redirect(flask.url_for("auth_login"))
  221. if form.validate_on_submit():
  222. user_obj.password = generate_hashed_value(form.password.data)
  223. user_obj.token = None
  224. flask.g.session.add(user_obj)
  225. try:
  226. flask.g.session.commit()
  227. flask.flash("Password changed")
  228. except SQLAlchemyError: # pragma: no cover
  229. flask.g.session.rollback()
  230. flask.flash("Could not set the new password.", "error")
  231. _log.exception("Password lost change - Error setting password.")
  232. return flask.redirect(flask.url_for("auth_login"))
  233. return flask.render_template(
  234. "login/password_reset.html", form=form, token=token
  235. )
  236. #
  237. # Methods specific to local login.
  238. #
  239. @UI_NS.route("/password/change/", methods=["GET", "POST"])
  240. @UI_NS.route("/password/change", methods=["GET", "POST"])
  241. @login_required
  242. def change_password():
  243. """ Method to change the password for local auth users.
  244. """
  245. form = forms.ChangePasswordForm()
  246. user_obj = pagure.lib.query.search_user(
  247. flask.g.session, username=flask.g.fas_user.username
  248. )
  249. if not user_obj:
  250. flask.abort(404, "User not found")
  251. if form.validate_on_submit():
  252. try:
  253. password_checks = check_password(
  254. form.old_password.data,
  255. user_obj.password,
  256. seed=pagure.config.config.get("PASSWORD_SEED", None),
  257. )
  258. except pagure.exceptions.PagureException as err:
  259. _log.exception(err)
  260. flask.flash(
  261. "Could not update your password, either user or password "
  262. "could not be checked",
  263. "error",
  264. )
  265. return flask.redirect(flask.url_for("auth_login"))
  266. if password_checks:
  267. user_obj.password = generate_hashed_value(form.password.data)
  268. flask.g.session.add(user_obj)
  269. else:
  270. flask.flash(
  271. "Could not update your password, either user or password "
  272. "could not be checked",
  273. "error",
  274. )
  275. return flask.redirect(flask.url_for("auth_login"))
  276. try:
  277. flask.g.session.commit()
  278. flask.flash("Password changed")
  279. except SQLAlchemyError: # pragma: no cover
  280. flask.g.session.rollback()
  281. flask.flash("Could not set the new password.", "error")
  282. _log.exception("Password change - Error setting new password.")
  283. return flask.redirect(flask.url_for("auth_login"))
  284. return flask.render_template("login/password_recover.html", form=form)
  285. def send_confirmation_email(user):
  286. """ Sends the confirmation email asking the user to confirm its email
  287. address.
  288. """
  289. if not user.emails:
  290. return
  291. url = pagure.config.config.get("APP_URL", flask.request.url_root)
  292. url = urljoin(
  293. url or flask.request.url_root,
  294. flask.url_for("ui_ns.confirm_user", token=user.token),
  295. )
  296. message = """ Dear %(username)s,
  297. Thank you for registering on pagure at %(url)s.
  298. To finish your registration, please click on the following link or copy/paste
  299. it in your browser:
  300. %(url)s
  301. You account will not be activated until you finish this step.
  302. Sincerely,
  303. Your pagure admin.
  304. """ % (
  305. {"username": user.username, "url": url}
  306. )
  307. pagure.lib.notify.send_email(
  308. text=message,
  309. subject="Confirm your user account",
  310. to_mail=user.emails[0].email,
  311. )
  312. def send_lostpassword_email(user):
  313. """ Sends the email with the information on how to reset his/her password
  314. to the user.
  315. """
  316. if not user.emails:
  317. return
  318. url = pagure.config.config.get("APP_URL", flask.request.url_root)
  319. url = urljoin(
  320. url or flask.request.url_root,
  321. flask.url_for("ui_ns.reset_password", token=user.token),
  322. )
  323. message = """ Dear %(username)s,
  324. The IP address %(ip)s has requested a password change for this account.
  325. If you wish to change your password, please click on the following link or
  326. copy/paste it in your browser:
  327. %(url)s
  328. If you did not request this change, please inform an admin immediately!
  329. Sincerely,
  330. Your pagure admin.
  331. """ % (
  332. {
  333. "username": user.username,
  334. "url": url,
  335. "ip": flask.request.remote_addr,
  336. }
  337. )
  338. pagure.lib.notify.send_email(
  339. text=message,
  340. subject="Confirm your password change",
  341. to_mail=user.emails[0].email,
  342. )
  343. def logout():
  344. """ Log the user out by expiring the user's session.
  345. """
  346. flask.g.fas_session_id = None
  347. flask.g.fas_user = None
  348. def _check_session_cookie():
  349. """ Set the user into flask.g if the user is logged in.
  350. """
  351. if not hasattr(flask.g, "session") or not flask.g.session:
  352. flask.g.session = pagure.lib.query.create_session(
  353. flask.current_app.config["DB_URL"]
  354. )
  355. cookie_name = pagure.config.config.get("SESSION_COOKIE_NAME", "pagure")
  356. cookie_name = "%s_local_cookie" % cookie_name
  357. session_id = None
  358. user = None
  359. login_time = None
  360. if cookie_name and cookie_name in flask.request.cookies:
  361. sessionid = flask.request.cookies.get(cookie_name)
  362. visit_session = pagure.lib.login.get_session_by_visitkey(
  363. flask.g.session, sessionid
  364. )
  365. if visit_session and visit_session.user:
  366. now = datetime.datetime.now()
  367. if now > visit_session.expiry:
  368. flask.flash("Session timed-out", "error")
  369. elif (
  370. pagure.config.config.get("CHECK_SESSION_IP", True)
  371. and visit_session.user_ip != flask.request.remote_addr
  372. ):
  373. flask.flash("Session expired", "error")
  374. else:
  375. new_expiry = now + datetime.timedelta(days=30)
  376. session_id = visit_session.visit_key
  377. user = visit_session.user
  378. login_time = visit_session.created
  379. visit_session.expiry = new_expiry
  380. flask.g.session.add(visit_session)
  381. try:
  382. flask.g.session.commit()
  383. except SQLAlchemyError as err: # pragma: no cover
  384. flask.flash(
  385. "Could not prolong the session in the db, "
  386. "please report this error to an admin",
  387. "error",
  388. )
  389. _log.exception(err)
  390. flask.g.fas_session_id = session_id
  391. if user:
  392. flask.g.fas_user = user
  393. flask.g.fas_user.email = user.default_email
  394. flask.g.authenticated = pagure.utils.authenticated()
  395. flask.g.fas_user.login_time = login_time
  396. def _send_session_cookie(response):
  397. """ Set the session cookie if the user is authenticated. """
  398. cookie_name = pagure.config.config.get("SESSION_COOKIE_NAME", "pagure")
  399. secure = pagure.config.config.get("SESSION_COOKIE_SECURE", True)
  400. response.set_cookie(
  401. key="%s_local_cookie" % cookie_name,
  402. value=flask.g.get("fas_session_id") or "",
  403. secure=secure,
  404. httponly=True,
  405. )
  406. return response