flask_app.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2014-2018 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import unicode_literals, absolute_import
  8. import datetime
  9. import gc
  10. import logging
  11. import time
  12. import os
  13. import flask
  14. import pygit2
  15. import pagure.doc_utils
  16. import pagure.exceptions
  17. import pagure.forms
  18. import pagure.lib.git
  19. import pagure.lib.query
  20. import pagure.login_forms
  21. import pagure.mail_logging
  22. import pagure.proxy
  23. import pagure.utils
  24. from pagure.config import config as pagure_config
  25. from pagure.utils import get_repo_path
  26. if os.environ.get("PAGURE_PERFREPO"):
  27. import pagure.perfrepo as perfrepo
  28. else:
  29. perfrepo = None
  30. logger = logging.getLogger(__name__)
  31. REDIS = None
  32. if (
  33. pagure_config["EVENTSOURCE_SOURCE"]
  34. or pagure_config["WEBHOOK"]
  35. or pagure_config.get("PAGURE_CI_SERVICES")
  36. ):
  37. pagure.lib.query.set_redis(
  38. host=pagure_config["REDIS_HOST"],
  39. port=pagure_config["REDIS_PORT"],
  40. dbname=pagure_config["REDIS_DB"],
  41. )
  42. if pagure_config.get("PAGURE_CI_SERVICES"):
  43. pagure.lib.query.set_pagure_ci(pagure_config["PAGURE_CI_SERVICES"])
  44. def create_app(config=None):
  45. """ Create the flask application. """
  46. app = flask.Flask(__name__)
  47. app.config = pagure_config
  48. if config:
  49. app.config.update(config)
  50. if app.config.get("SESSION_TYPE", None) is not None:
  51. import flask_session
  52. flask_session.Session(app)
  53. pagure.utils.set_up_logging(app=app)
  54. @app.errorhandler(500)
  55. def fatal_error(error): # pragma: no cover
  56. """500 Fatal Error page"""
  57. logger.exception("Error while processing request")
  58. return flask.render_template("fatal_error.html", error=error), 500
  59. app.jinja_env.trim_blocks = True
  60. app.jinja_env.lstrip_blocks = True
  61. if perfrepo:
  62. # Do this as early as possible.
  63. # We want the perfrepo before_request to be the very first thing
  64. # to be run, so that we can properly setup the stats before the
  65. # request.
  66. app.before_request(perfrepo.reset_stats)
  67. auth = pagure_config.get("PAGURE_AUTH", None)
  68. if auth in ["fas", "openid"]:
  69. # Only import and set flask_fas_openid if it is needed
  70. from pagure.ui.fas_login import FAS
  71. FAS.init_app(app)
  72. elif auth == "oidc":
  73. # Only import and set flask_fas_openid if it is needed
  74. from pagure.ui.oidc_login import oidc, fas_user_from_oidc
  75. oidc.init_app(app)
  76. app.before_request(fas_user_from_oidc)
  77. if auth == "local":
  78. # Only import the login controller if the app is set up for local login
  79. import pagure.ui.login as login
  80. app.before_request(login._check_session_cookie)
  81. app.after_request(login._send_session_cookie)
  82. # Support proxy
  83. app.wsgi_app = pagure.proxy.ReverseProxied(app.wsgi_app)
  84. # Back port 'equalto' to older version of jinja2
  85. app.jinja_env.tests.setdefault(
  86. "equalto", lambda value, other: value == other
  87. )
  88. # Import the application
  89. from pagure.api import API # noqa: E402
  90. app.register_blueprint(API)
  91. from pagure.ui import UI_NS # noqa: E402
  92. app.register_blueprint(UI_NS)
  93. from pagure.internal import PV # noqa: E402
  94. app.register_blueprint(PV)
  95. # Import 3rd party blueprints
  96. plugin_config = flask.config.Config("")
  97. if "PAGURE_PLUGIN" in os.environ:
  98. plugin_config.from_envvar("PAGURE_PLUGIN")
  99. for blueprint in plugin_config.get("PLUGINS") or []:
  100. logger.info("Loading blueprint: %s", blueprint.name)
  101. app.register_blueprint(blueprint)
  102. themename = pagure_config.get("THEME", "default")
  103. here = os.path.abspath(
  104. os.path.join(os.path.dirname(os.path.abspath(__file__)))
  105. )
  106. themeblueprint = flask.Blueprint(
  107. "theme",
  108. __name__,
  109. static_url_path="/theme/static",
  110. static_folder=os.path.join(here, "themes", themename, "static"),
  111. )
  112. # Jinja can be told to look for templates in different folders
  113. # That's what we do here
  114. template_folders = os.path.join(
  115. app.root_path,
  116. app.template_folder,
  117. os.path.join(here, "themes", themename, "templates"),
  118. )
  119. import jinja2
  120. # Jinja looks for the template in the order of the folders specified
  121. templ_loaders = [
  122. jinja2.FileSystemLoader(template_folders),
  123. app.jinja_loader,
  124. ]
  125. app.jinja_loader = jinja2.ChoiceLoader(templ_loaders)
  126. app.register_blueprint(themeblueprint)
  127. app.before_request(set_request)
  128. app.teardown_request(end_request)
  129. if perfrepo:
  130. # Do this at the very end, so that this after_request comes last.
  131. app.after_request(perfrepo.print_stats)
  132. app.add_url_rule("/login/", view_func=auth_login, methods=["GET", "POST"])
  133. app.add_url_rule("/logout/", view_func=auth_logout)
  134. return app
  135. def generate_user_key_files():
  136. """ Regenerate the key files used by gitolite.
  137. """
  138. gitolite_home = pagure_config.get("GITOLITE_HOME", None)
  139. if gitolite_home:
  140. users = pagure.lib.query.search_user(flask.g.session)
  141. for user in users:
  142. pagure.lib.query.update_user_ssh(
  143. flask.g.session,
  144. user,
  145. None,
  146. pagure_config.get("GITOLITE_KEYDIR", None),
  147. update_only=True,
  148. )
  149. pagure.lib.git.generate_gitolite_acls(project=None)
  150. def admin_session_timedout():
  151. """ Check if the current user has been authenticated for more than what
  152. is allowed (defaults to 15 minutes).
  153. If it is the case, the user is logged out and the method returns True,
  154. otherwise it returns False.
  155. """
  156. timedout = False
  157. if not pagure.utils.authenticated():
  158. return True
  159. login_time = flask.g.fas_user.login_time
  160. # This is because flask_fas_openid will store this as a posix timestamp
  161. if not isinstance(login_time, datetime.datetime):
  162. login_time = datetime.datetime.utcfromtimestamp(login_time)
  163. if (datetime.datetime.utcnow() - login_time) > pagure_config.get(
  164. "ADMIN_SESSION_LIFETIME", datetime.timedelta(minutes=15)
  165. ):
  166. timedout = True
  167. logout()
  168. return timedout
  169. def logout():
  170. """ Log out the user currently logged in in the application
  171. """
  172. auth = pagure_config.get("PAGURE_AUTH", None)
  173. if auth in ["fas", "openid"]:
  174. if hasattr(flask.g, "fas_user") and flask.g.fas_user is not None:
  175. from pagure.ui.fas_login import FAS
  176. FAS.logout()
  177. elif auth == "oidc":
  178. from pagure.ui.oidc_login import oidc_logout
  179. oidc_logout()
  180. elif auth == "local":
  181. import pagure.ui.login as login
  182. login.logout()
  183. def set_request():
  184. """ Prepare every request. """
  185. flask.session.permanent = True
  186. if not hasattr(flask.g, "session") or not flask.g.session:
  187. flask.g.session = pagure.lib.query.create_session(
  188. flask.current_app.config["DB_URL"]
  189. )
  190. flask.g.version = pagure.__version__
  191. flask.g.confirmationform = pagure.forms.ConfirmationForm()
  192. # The API namespace has its own way of getting repo and username and
  193. # of handling errors
  194. if flask.request.blueprint == "api_ns":
  195. return
  196. flask.g.forkbuttonform = None
  197. if pagure.utils.authenticated():
  198. flask.g.forkbuttonform = pagure.forms.ConfirmationForm()
  199. # Force logout if current session started before users'
  200. # refuse_sessions_before
  201. login_time = flask.g.fas_user.login_time
  202. # This is because flask_fas_openid will store this as a posix timestamp
  203. if not isinstance(login_time, datetime.datetime):
  204. login_time = datetime.datetime.utcfromtimestamp(login_time)
  205. user = _get_user(username=flask.g.fas_user.username)
  206. if (
  207. user.refuse_sessions_before
  208. and login_time < user.refuse_sessions_before
  209. ):
  210. logout()
  211. return flask.redirect(flask.url_for("ui_ns.index"))
  212. flask.g.justlogedout = flask.session.get("_justloggedout", False)
  213. if flask.g.justlogedout:
  214. flask.session["_justloggedout"] = None
  215. flask.g.new_user = False
  216. if flask.session.get("_new_user"):
  217. flask.g.new_user = True
  218. flask.session["_new_user"] = False
  219. flask.g.authenticated = pagure.utils.authenticated()
  220. flask.g.admin = pagure.utils.is_admin()
  221. # Retrieve the variables in the URL
  222. args = flask.request.view_args or {}
  223. # Check if there is a `repo` and an `username`
  224. repo = args.get("repo")
  225. username = args.get("username")
  226. namespace = args.get("namespace")
  227. # If there isn't a `repo` in the URL path, or if there is but the
  228. # endpoint called is part of the API, just don't do anything
  229. if repo:
  230. flask.g.repo = pagure.lib.query.get_authorized_project(
  231. flask.g.session, repo, user=username, namespace=namespace
  232. )
  233. if flask.g.authenticated:
  234. flask.g.repo_forked = pagure.lib.query.get_authorized_project(
  235. flask.g.session,
  236. repo,
  237. user=flask.g.fas_user.username,
  238. namespace=namespace,
  239. )
  240. flask.g.repo_starred = pagure.lib.query.has_starred(
  241. flask.g.session, flask.g.repo, user=flask.g.fas_user.username
  242. )
  243. if (
  244. not flask.g.repo
  245. and namespace
  246. and pagure_config.get("OLD_VIEW_COMMIT_ENABLED", False)
  247. and len(repo) == 40
  248. ):
  249. return flask.redirect(
  250. flask.url_for(
  251. "ui_ns.view_commit",
  252. repo=namespace,
  253. commitid=repo,
  254. username=username,
  255. namespace=None,
  256. )
  257. )
  258. if flask.g.repo is None:
  259. flask.abort(404, "Project not found")
  260. flask.g.reponame = get_repo_path(flask.g.repo)
  261. flask.g.repo_obj = pygit2.Repository(flask.g.reponame)
  262. flask.g.repo_admin = pagure.utils.is_repo_admin(flask.g.repo)
  263. flask.g.repo_committer = pagure.utils.is_repo_committer(flask.g.repo)
  264. flask.g.repo_user = pagure.utils.is_repo_user(flask.g.repo)
  265. flask.g.branches = sorted(flask.g.repo_obj.listall_branches())
  266. repouser = flask.g.repo.user.user if flask.g.repo.is_fork else None
  267. fas_user = flask.g.fas_user if pagure.utils.authenticated() else None
  268. flask.g.repo_watch_levels = pagure.lib.query.get_watch_level_on_repo(
  269. flask.g.session,
  270. fas_user,
  271. flask.g.repo.name,
  272. repouser=repouser,
  273. namespace=namespace,
  274. )
  275. items_per_page = pagure_config["ITEM_PER_PAGE"]
  276. flask.g.offset = 0
  277. flask.g.page = 1
  278. flask.g.limit = items_per_page
  279. page = flask.request.args.get("page")
  280. limit = flask.request.args.get("n")
  281. if limit:
  282. try:
  283. limit = int(limit)
  284. except ValueError:
  285. limit = 10
  286. if limit > 500 or limit <= 0:
  287. limit = items_per_page
  288. flask.g.limit = limit
  289. if page:
  290. try:
  291. page = abs(int(page))
  292. except ValueError:
  293. page = 1
  294. if page <= 0:
  295. page = 1
  296. flask.g.page = page
  297. flask.g.offset = (page - 1) * flask.g.limit
  298. def auth_login(): # pragma: no cover
  299. """ Method to log into the application using FAS OpenID. """
  300. return_point = flask.url_for("ui_ns.index")
  301. if "next" in flask.request.args:
  302. if pagure.utils.is_safe_url(flask.request.args["next"]):
  303. return_point = flask.request.args["next"]
  304. authenticated = pagure.utils.authenticated()
  305. auth = pagure_config.get("PAGURE_AUTH", None)
  306. if not authenticated and auth == "oidc":
  307. from pagure.ui.oidc_login import oidc, fas_user_from_oidc, set_user
  308. # If oidc is used and user hits this endpoint, it will redirect
  309. # to IdP with destination=<pagure>/login?next=<location>
  310. # After confirming user identity, the IdP will redirect user here
  311. # again, but this time oidc.user_loggedin will be True and thus
  312. # execution will go through the else clause, making the Pagure
  313. # authentication machinery pick the user up
  314. if not oidc.user_loggedin:
  315. return oidc.redirect_to_auth_server(flask.request.url)
  316. else:
  317. flask.session["oidc_logintime"] = time.time()
  318. fas_user_from_oidc()
  319. authenticated = pagure.utils.authenticated()
  320. set_user()
  321. if authenticated:
  322. return flask.redirect(return_point)
  323. admins = pagure_config["ADMIN_GROUP"]
  324. if isinstance(admins, list):
  325. admins = set(admins)
  326. else: # pragma: no cover
  327. admins = set([admins])
  328. if auth in ["fas", "openid"]:
  329. from pagure.ui.fas_login import FAS
  330. groups = set()
  331. if not pagure_config.get("ENABLE_GROUP_MNGT", False):
  332. groups = [
  333. group.group_name
  334. for group in pagure.lib.query.search_groups(
  335. flask.g.session, group_type="user"
  336. )
  337. ]
  338. groups = set(groups).union(admins)
  339. ext_committer = set(pagure_config.get("EXTERNAL_COMMITTER", {}))
  340. groups = set(groups).union(ext_committer)
  341. return FAS.login(return_url=return_point, groups=groups)
  342. elif auth == "local":
  343. form = pagure.login_forms.LoginForm()
  344. return flask.render_template(
  345. "login/login.html", next_url=return_point, form=form
  346. )
  347. def auth_logout(): # pragma: no cover
  348. """ Method to log out from the application. """
  349. return_point = flask.url_for("ui_ns.index")
  350. if "next" in flask.request.args:
  351. if pagure.utils.is_safe_url(flask.request.args["next"]):
  352. return_point = flask.request.args["next"]
  353. if not pagure.utils.authenticated():
  354. return flask.redirect(return_point)
  355. logout()
  356. flask.flash("You have been logged out")
  357. flask.session["_justloggedout"] = True
  358. return flask.redirect(return_point)
  359. # pylint: disable=unused-argument
  360. def end_request(exception=None):
  361. """ This method is called at the end of each request.
  362. Remove the DB session at the end of each request.
  363. Runs a garbage collection to get rid of any open pygit2 handles.
  364. Details: https://pagure.io/pagure/issue/2302
  365. """
  366. flask.g.session.remove()
  367. gc.collect()
  368. def _get_user(username):
  369. """ Check if user exists or not
  370. """
  371. try:
  372. return pagure.lib.query.get_user(flask.g.session, username)
  373. except pagure.exceptions.PagureException as e:
  374. flask.abort(404, "%s" % e)