flask_app.py 18 KB

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