login.py 15 KB

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