project.py 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236
  1. # -*- coding: utf-8 -*-
  2. """
  3. (c) 2015-2019 - Copyright Red Hat Inc
  4. Authors:
  5. Pierre-Yves Chibon <pingou@pingoured.fr>
  6. """
  7. from __future__ import unicode_literals, absolute_import
  8. import flask
  9. import logging
  10. from sqlalchemy.exc import SQLAlchemyError
  11. from six import string_types
  12. from pygit2 import GitError, Repository
  13. import pagure
  14. import pagure.forms
  15. import pagure.exceptions
  16. import pagure.lib.git
  17. import pagure.lib.query
  18. import pagure.utils
  19. from pagure.api import (
  20. API,
  21. api_method,
  22. APIERROR,
  23. api_login_required,
  24. get_authorized_api_project,
  25. api_login_optional,
  26. get_request_data,
  27. get_page,
  28. get_per_page,
  29. )
  30. from pagure.api.utils import _get_repo, _check_token
  31. from pagure.config import config as pagure_config
  32. _log = logging.getLogger(__name__)
  33. @API.route("/<repo>/git/tags")
  34. @API.route("/<namespace>/<repo>/git/tags")
  35. @API.route("/fork/<username>/<repo>/git/tags")
  36. @API.route("/fork/<username>/<namespace>/<repo>/git/tags")
  37. @api_method
  38. def api_git_tags(repo, username=None, namespace=None):
  39. """
  40. Project git tags
  41. ----------------
  42. List the tags made on the project Git repository.
  43. ::
  44. GET /api/0/<repo>/git/tags
  45. GET /api/0/<namespace>/<repo>/git/tags
  46. ::
  47. GET /api/0/fork/<username>/<repo>/git/tags
  48. GET /api/0/fork/<username>/<namespace>/<repo>/git/tags
  49. Parameters
  50. ^^^^^^^^^^
  51. +-----------------+----------+---------------+--------------------------+
  52. | Key | Type | Optionality | Description |
  53. +=================+==========+===============+==========================+
  54. | ``with_commits``| string | Optional | | Include the commit hash|
  55. | | | | corresponding to the |
  56. | | | | tags found in the repo |
  57. +-----------------+----------+---------------+--------------------------+
  58. Sample response
  59. ^^^^^^^^^^^^^^^
  60. ::
  61. {
  62. "total_tags": 2,
  63. "tags": ["0.0.1", "0.0.2"]
  64. }
  65. {
  66. "total_tags": 2,
  67. "tags": {
  68. "0.0.1": "bb8fa2aa199da08d6085e1c9badc3d83d188d38c",
  69. "0.0.2": "d16fe107eca31a1bdd66fb32c6a5c568e45b627e"
  70. }
  71. }
  72. """
  73. with_commits = pagure.utils.is_true(
  74. flask.request.values.get("with_commits", False)
  75. )
  76. repo = _get_repo(repo, username, namespace)
  77. tags = pagure.lib.git.get_git_tags(repo, with_commits=with_commits)
  78. jsonout = flask.jsonify({"total_tags": len(tags), "tags": tags})
  79. return jsonout
  80. @API.route("/<repo>/watchers")
  81. @API.route("/<namespace>/<repo>/watchers")
  82. @API.route("/fork/<username>/<repo>/watchers")
  83. @API.route("/fork/<username>/<namespace>/<repo>/watchers")
  84. @api_method
  85. def api_project_watchers(repo, username=None, namespace=None):
  86. """
  87. Project watchers
  88. ----------------
  89. List the watchers on the project.
  90. ::
  91. GET /api/0/<repo>/watchers
  92. GET /api/0/<namespace>/<repo>/watchers
  93. ::
  94. GET /api/0/fork/<username>/<repo>/watchers
  95. GET /api/0/fork/<username>/<namespace>/<repo>/watchers
  96. Sample response
  97. ^^^^^^^^^^^^^^^
  98. ::
  99. {
  100. "total_watchers": 1,
  101. "watchers": {
  102. "mprahl": [
  103. "issues",
  104. "commits"
  105. ]
  106. }
  107. }
  108. """
  109. repo = _get_repo(repo, username, namespace)
  110. implicit_watch_users = set([repo.user.username])
  111. for access_type in repo.access_users:
  112. implicit_watch_users = implicit_watch_users.union(
  113. set([user.username for user in repo.access_users[access_type]])
  114. )
  115. watching_users_to_watch_level = {}
  116. for implicit_watch_user in implicit_watch_users:
  117. user_watch_level = pagure.lib.query.get_watch_level_on_repo(
  118. flask.g.session, implicit_watch_user, repo
  119. )
  120. watching_users_to_watch_level[implicit_watch_user] = user_watch_level
  121. for access_type in repo.access_groups.keys():
  122. group_names = [
  123. "@" + group.group_name for group in repo.access_groups[access_type]
  124. ]
  125. for group_name in group_names:
  126. if group_name not in watching_users_to_watch_level:
  127. watching_users_to_watch_level[group_name] = set()
  128. # By the logic in pagure.lib.query.get_watch_level_on_repo, group
  129. # members only by default watch issues. If they want to watch
  130. # commits they have to explicitly subscribe.
  131. watching_users_to_watch_level[group_name].add("issues")
  132. for key in watching_users_to_watch_level:
  133. watching_users_to_watch_level[key] = list(
  134. watching_users_to_watch_level[key]
  135. )
  136. # Get the explicit watch statuses
  137. for watcher in repo.watchers:
  138. if watcher.watch_issues or watcher.watch_commits:
  139. watching_users_to_watch_level[
  140. watcher.user.username
  141. ] = pagure.lib.query.get_watch_level_on_repo(
  142. flask.g.session, watcher.user.username, repo
  143. )
  144. else:
  145. if watcher.user.username in watching_users_to_watch_level:
  146. watching_users_to_watch_level.pop(watcher.user.username, None)
  147. return flask.jsonify(
  148. {
  149. "total_watchers": len(watching_users_to_watch_level),
  150. "watchers": watching_users_to_watch_level,
  151. }
  152. )
  153. @API.route("/<repo>/git/urls")
  154. @API.route("/<namespace>/<repo>/git/urls")
  155. @API.route("/fork/<username>/<repo>/git/urls")
  156. @API.route("/fork/<username>/<namespace>/<repo>/git/urls")
  157. @api_login_optional()
  158. @api_method
  159. def api_project_git_urls(repo, username=None, namespace=None):
  160. """
  161. Project Git URLs
  162. ----------------
  163. List the Git URLS on the project.
  164. ::
  165. GET /api/0/<repo>/git/urls
  166. GET /api/0/<namespace>/<repo>/git/urls
  167. ::
  168. GET /api/0/fork/<username>/<repo>/git/urls
  169. GET /api/0/fork/<username>/<namespace>/<repo>/git/urls
  170. Sample response
  171. ^^^^^^^^^^^^^^^
  172. ::
  173. {
  174. "total_urls": 2,
  175. "urls": {
  176. "ssh": "ssh://git@pagure.io/mprahl-test123.git",
  177. "git": "https://pagure.io/mprahl-test123.git"
  178. }
  179. }
  180. """
  181. repo = _get_repo(repo, username, namespace)
  182. git_urls = {}
  183. git_url_ssh = pagure_config.get("GIT_URL_SSH")
  184. if pagure.utils.api_authenticated() and git_url_ssh:
  185. try:
  186. git_url_ssh = git_url_ssh.format(
  187. username=flask.g.fas_user.username
  188. )
  189. except (KeyError, IndexError):
  190. pass
  191. if git_url_ssh:
  192. git_urls["ssh"] = "{0}{1}.git".format(git_url_ssh, repo.fullname)
  193. if pagure_config.get("GIT_URL_GIT"):
  194. git_urls["git"] = "{0}{1}.git".format(
  195. pagure_config["GIT_URL_GIT"], repo.fullname
  196. )
  197. return flask.jsonify({"total_urls": len(git_urls), "urls": git_urls})
  198. @API.route("/<repo>/git/branches")
  199. @API.route("/<namespace>/<repo>/git/branches")
  200. @API.route("/fork/<username>/<repo>/git/branches")
  201. @API.route("/fork/<username>/<namespace>/<repo>/git/branches")
  202. @api_method
  203. def api_git_branches(repo, username=None, namespace=None):
  204. """
  205. List project branches
  206. ---------------------
  207. List the branches associated with a Pagure git repository
  208. ::
  209. GET /api/0/<repo>/git/branches
  210. GET /api/0/<namespace>/<repo>/git/branches
  211. ::
  212. GET /api/0/fork/<username>/<repo>/git/branches
  213. GET /api/0/fork/<username>/<namespace>/<repo>/git/branches
  214. Sample response
  215. ^^^^^^^^^^^^^^^
  216. ::
  217. {
  218. "total_branches": 2,
  219. "branches": ["master", "dev"]
  220. }
  221. """
  222. repo = _get_repo(repo, username, namespace)
  223. branches = pagure.lib.git.get_git_branches(repo)
  224. return flask.jsonify(
  225. {"total_branches": len(branches), "branches": branches}
  226. )
  227. @API.route("/projects")
  228. @api_method
  229. def api_projects():
  230. """
  231. List projects
  232. --------------
  233. Search projects given the specified criterias.
  234. ::
  235. GET /api/0/projects
  236. ::
  237. GET /api/0/projects?tags=fedora-infra
  238. ::
  239. GET /api/0/projects?page=1&per_page=50
  240. Parameters
  241. ^^^^^^^^^^
  242. +---------------+----------+---------------+--------------------------+
  243. | Key | Type | Optionality | Description |
  244. +===============+==========+===============+==========================+
  245. | ``tags`` | string | Optional | | Filters the projects |
  246. | | | | returned by their tags |
  247. +---------------+----------+---------------+--------------------------+
  248. | ``pattern`` | string | Optional | | Filters the projects |
  249. | | | | by the pattern string |
  250. +---------------+----------+---------------+--------------------------+
  251. | ``username`` | string | Optional | | Filters the projects |
  252. | | | | returned by the users |
  253. | | | | having commit rights |
  254. | | | | to it |
  255. +---------------+----------+---------------+--------------------------+
  256. | ``owner`` | string | Optional | | Filters the projects |
  257. | | | | by ownership. |
  258. | | | | If the argument is of |
  259. | | | | the form <!owner> then |
  260. | | | | the project returned |
  261. | | | | are the ones *not* |
  262. | | | | owned by this user. |
  263. +---------------+----------+---------------+--------------------------+
  264. | ``namespace`` | string | Optional | | Filters the projects |
  265. | | | | by namespace |
  266. +---------------+----------+---------------+--------------------------+
  267. | ``fork`` | boolean | Optional | | Filters the projects |
  268. | | | | returned depending if |
  269. | | | | they are forks or not |
  270. +---------------+----------+---------------+--------------------------+
  271. | ``short`` | boolean | Optional | | Whether to return the |
  272. | | | | entrie project JSON |
  273. | | | | or just a sub-set |
  274. +---------------+----------+---------------+--------------------------+
  275. | ``page`` | int | Optional | | Specifies which |
  276. | | | | page to return |
  277. | | | | (defaults to: 1) |
  278. +---------------+----------+---------------+--------------------------+
  279. | ``per_page`` | int | Optional | | The number of projects |
  280. | | | | to return per page. |
  281. | | | | The maximum is 100. |
  282. +---------------+----------+---------------+--------------------------+
  283. Sample response
  284. ^^^^^^^^^^^^^^^
  285. ::
  286. {
  287. "args": {
  288. "fork": null,
  289. "namespace": null,
  290. "owner": null,
  291. "page": 1,
  292. "pattern": null,
  293. "per_page": 2,
  294. "short": false,
  295. "tags": [],
  296. "username": null
  297. },
  298. "pagination": {
  299. "first": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=1",
  300. "last": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=500",
  301. "next": "http://127.0.0.1:5000/api/0/projects?per_page=2&page=2",
  302. "page": 1,
  303. "pages": 500,
  304. "per_page": 2,
  305. "prev": null
  306. },
  307. "projects": [
  308. {
  309. "access_groups": {
  310. "admin": [],
  311. "commit": [],
  312. "ticket": []
  313. },
  314. "access_users": {
  315. "admin": [],
  316. "commit": [],
  317. "owner": [
  318. "mprahl"
  319. ],
  320. "ticket": []
  321. },
  322. "close_status": [],
  323. "custom_keys": [],
  324. "date_created": "1498841289",
  325. "description": "test1",
  326. "fullname": "test1",
  327. "id": 1,
  328. "milestones": {},
  329. "name": "test1",
  330. "namespace": null,
  331. "parent": null,
  332. "priorities": {},
  333. "tags": [],
  334. "url_path": "test1",
  335. "user": {
  336. "fullname": "Matt Prahl",
  337. "name": "mprahl"
  338. }
  339. },
  340. {
  341. "access_groups": {
  342. "admin": [],
  343. "commit": [],
  344. "ticket": []
  345. },
  346. "access_users": {
  347. "admin": [],
  348. "commit": [],
  349. "owner": [
  350. "mprahl"
  351. ],
  352. "ticket": []
  353. },
  354. "close_status": [],
  355. "custom_keys": [],
  356. "date_created": "1499795310",
  357. "description": "test2",
  358. "fullname": "test2",
  359. "id": 2,
  360. "milestones": {},
  361. "name": "test2",
  362. "namespace": null,
  363. "parent": null,
  364. "priorities": {},
  365. "tags": [],
  366. "url_path": "test2",
  367. "user": {
  368. "fullname": "Matt Prahl",
  369. "name": "mprahl"
  370. }
  371. }
  372. ],
  373. "total_projects": 1000
  374. }
  375. """
  376. tags = flask.request.values.getlist("tags")
  377. username = flask.request.values.get("username", None)
  378. fork = flask.request.values.get("fork", None)
  379. namespace = flask.request.values.get("namespace", None)
  380. owner = flask.request.values.get("owner", None)
  381. pattern = flask.request.values.get("pattern", None)
  382. short = pagure.utils.is_true(flask.request.values.get("short", False))
  383. if fork is not None:
  384. fork = pagure.utils.is_true(fork)
  385. private = False
  386. if pagure.utils.authenticated() and username == flask.g.fas_user.username:
  387. private = flask.g.fas_user.username
  388. project_count = pagure.lib.query.search_projects(
  389. flask.g.session,
  390. username=username,
  391. fork=fork,
  392. tags=tags,
  393. pattern=pattern,
  394. private=private,
  395. namespace=namespace,
  396. owner=owner,
  397. count=True,
  398. )
  399. # Pagination code inspired by Flask-SQLAlchemy
  400. page = get_page()
  401. per_page = get_per_page()
  402. pagination_metadata = pagure.lib.query.get_pagination_metadata(
  403. flask.request, page, per_page, project_count
  404. )
  405. query_start = (page - 1) * per_page
  406. query_limit = per_page
  407. projects = pagure.lib.query.search_projects(
  408. flask.g.session,
  409. username=username,
  410. fork=fork,
  411. tags=tags,
  412. pattern=pattern,
  413. private=private,
  414. namespace=namespace,
  415. owner=owner,
  416. limit=query_limit,
  417. start=query_start,
  418. )
  419. # prepare the output json
  420. jsonout = {
  421. "total_projects": project_count,
  422. "projects": projects,
  423. "args": {
  424. "tags": tags,
  425. "username": username,
  426. "fork": fork,
  427. "pattern": pattern,
  428. "namespace": namespace,
  429. "owner": owner,
  430. "short": short,
  431. },
  432. }
  433. if not short:
  434. projects = [p.to_json(api=True, public=True) for p in projects]
  435. else:
  436. projects = [
  437. {
  438. "name": p.name,
  439. "namespace": p.namespace,
  440. "fullname": p.fullname.replace("forks/", "fork/", 1)
  441. if p.fullname.startswith("forks/")
  442. else p.fullname,
  443. "description": p.description,
  444. }
  445. for p in projects
  446. ]
  447. jsonout["projects"] = projects
  448. if pagination_metadata:
  449. jsonout["args"]["page"] = page
  450. jsonout["args"]["per_page"] = per_page
  451. jsonout["pagination"] = pagination_metadata
  452. return flask.jsonify(jsonout)
  453. @API.route("/<repo>")
  454. @API.route("/<namespace>/<repo>")
  455. @API.route("/fork/<username>/<repo>")
  456. @API.route("/fork/<username>/<namespace>/<repo>")
  457. @api_method
  458. def api_project(repo, username=None, namespace=None):
  459. """
  460. Project information
  461. -------------------
  462. Return information about a specific project
  463. ::
  464. GET /api/0/<repo>
  465. GET /api/0/<namespace>/<repo>
  466. ::
  467. GET /api/0/fork/<username>/<repo>
  468. GET /api/0/fork/<username>/<namespace>/<repo>
  469. Sample response
  470. ^^^^^^^^^^^^^^^
  471. ::
  472. {
  473. "access_groups": {
  474. "admin": [],
  475. "commit": [],
  476. "ticket": []
  477. },
  478. "access_users": {
  479. "admin": [
  480. "ryanlerch"
  481. ],
  482. "commit": [
  483. "puiterwijk"
  484. ],
  485. "owner": [
  486. "pingou"
  487. ],
  488. "ticket": [
  489. "vivekanand1101",
  490. "mprahl",
  491. "jcline",
  492. "lslebodn",
  493. "cverna",
  494. "farhaan"
  495. ]
  496. },
  497. "close_status": [
  498. "Invalid",
  499. "Insufficient data",
  500. "Fixed",
  501. "Duplicate"
  502. ],
  503. "custom_keys": [],
  504. "date_created": "1431549490",
  505. "date_modified": "1431549490",
  506. "description": "A git centered forge",
  507. "fullname": "pagure",
  508. "id": 10,
  509. "milestones": {},
  510. "name": "pagure",
  511. "namespace": null,
  512. "parent": null,
  513. "priorities": {},
  514. "tags": [
  515. "pagure",
  516. "fedmsg"
  517. ],
  518. "user": {
  519. "fullname": "Pierre-YvesChibon",
  520. "name": "pingou"
  521. }
  522. }
  523. """
  524. repo = _get_repo(repo, username, namespace)
  525. expand_group = pagure.utils.is_true(
  526. flask.request.values.get("expand_group", False)
  527. )
  528. output = repo.to_json(api=True, public=True)
  529. if expand_group:
  530. group_details = {}
  531. for grp in repo.projects_groups:
  532. group_details[grp.group.group_name] = [
  533. user.username for user in grp.group.users
  534. ]
  535. output["group_details"] = group_details
  536. jsonout = flask.jsonify(output)
  537. return jsonout
  538. @API.route("/new/", methods=["POST"])
  539. @API.route("/new", methods=["POST"])
  540. @api_login_required(acls=["create_project"])
  541. @api_method
  542. def api_new_project():
  543. """
  544. Create a new project
  545. --------------------
  546. Create a new project on this pagure instance.
  547. This is an asynchronous call.
  548. ::
  549. POST /api/0/new
  550. Input
  551. ^^^^^
  552. +----------------------------+---------+--------------+---------------------------+
  553. | Key | Type | Optionality | Description |
  554. +============================+=========+==============+===========================+
  555. | ``name`` | string | Mandatory | | The name of the new |
  556. | | | | project. |
  557. +----------------------------+---------+--------------+---------------------------+
  558. | ``description`` | string | Mandatory | | A short description of |
  559. | | | | the new project. |
  560. +----------------------------+---------+--------------+---------------------------+
  561. | ``namespace`` | string | Optional | | The namespace of the |
  562. | | | | project to fork. |
  563. +----------------------------+---------+--------------+---------------------------+
  564. | ``url`` | string | Optional | | An url providing more |
  565. | | | | information about the |
  566. | | | | project. |
  567. +----------------------------+---------+--------------+---------------------------+
  568. | ``avatar_email`` | string | Optional | | An email address for the|
  569. | | | | avatar of the project. |
  570. +----------------------------+---------+--------------+---------------------------+
  571. | ``create_readme`` | boolean | Optional | | A boolean to specify if |
  572. | | | | there should be a readme|
  573. | | | | added to the project on |
  574. | | | | creation. |
  575. +----------------------------+---------+--------------+---------------------------+
  576. | ``private`` | boolean | Optional | | A boolean to specify if |
  577. | | | | the project to create |
  578. | | | | is private. |
  579. | | | | Note: not all pagure |
  580. | | | | instance support private|
  581. | | | | projects, confirm this |
  582. | | | | with your administrators|
  583. +----------------------------+---------+--------------+---------------------------+
  584. | ``ignore_existing_repos`` | boolean | Optional | | Only available to admins|
  585. | | | | this option allows them |
  586. | | | | to make project creation|
  587. | | | | pass even if there is |
  588. | | | | already a coresopnding |
  589. | | | | git repository on disk |
  590. +----------------------------+---------+--------------+---------------------------+
  591. | ``repospanner_region`` | boolean | Optional | | Only available to admins|
  592. | | | | this option allows them |
  593. | | | | to override the default |
  594. | | | | respoSpanner region |
  595. | | | | configured |
  596. +----------------------------+---------+--------------+---------------------------+
  597. | ``wait`` | boolean | Optional | | A boolean to specify if |
  598. | | | | this API call should |
  599. | | | | return a taskid or if it|
  600. | | | | should wait for the task|
  601. | | | | to finish. |
  602. +----------------------------+---------+--------------+---------------------------+
  603. Sample response
  604. ^^^^^^^^^^^^^^^
  605. ::
  606. wait=False:
  607. {
  608. 'message': 'Project creation queued',
  609. 'taskid': '123-abcd'
  610. }
  611. wait=True:
  612. {
  613. 'message': 'Project creation queued'
  614. }
  615. """ # noqa
  616. user = pagure.lib.query.search_user(
  617. flask.g.session, username=flask.g.fas_user.username
  618. )
  619. output = {}
  620. if not pagure_config.get("ENABLE_NEW_PROJECTS", True):
  621. raise pagure.exceptions.APIError(
  622. 404, error_code=APIERROR.ENEWPROJECTDISABLED
  623. )
  624. namespaces = pagure_config["ALLOWED_PREFIX"][:]
  625. if user:
  626. namespaces.extend([grp for grp in user.groups])
  627. form = pagure.forms.ProjectForm(namespaces=namespaces, csrf_enabled=False)
  628. if form.validate_on_submit():
  629. name = form.name.data
  630. description = form.description.data
  631. namespace = form.namespace.data
  632. url = form.url.data
  633. avatar_email = form.avatar_email.data
  634. create_readme = form.create_readme.data
  635. if namespace:
  636. namespace = namespace.strip()
  637. private = False
  638. if pagure_config.get("PRIVATE_PROJECTS", False):
  639. private = form.private.data
  640. if form.repospanner_region:
  641. repospanner_region = form.repospanner_region.data
  642. else:
  643. repospanner_region = None
  644. if form.ignore_existing_repos:
  645. ignore_existing_repos = form.ignore_existing_repos.data
  646. else:
  647. ignore_existing_repos = False
  648. try:
  649. task = pagure.lib.query.new_project(
  650. flask.g.session,
  651. name=name,
  652. namespace=namespace,
  653. repospanner_region=repospanner_region,
  654. ignore_existing_repo=ignore_existing_repos,
  655. description=description,
  656. private=private,
  657. url=url,
  658. avatar_email=avatar_email,
  659. user=flask.g.fas_user.username,
  660. blacklist=pagure_config["BLACKLISTED_PROJECTS"],
  661. allowed_prefix=pagure_config["ALLOWED_PREFIX"],
  662. add_readme=create_readme,
  663. userobj=user,
  664. prevent_40_chars=pagure_config.get(
  665. "OLD_VIEW_COMMIT_ENABLED", False
  666. ),
  667. user_ns=pagure_config.get("USER_NAMESPACE", False),
  668. )
  669. flask.g.session.commit()
  670. output = {"message": "Project creation queued", "taskid": task.id}
  671. if get_request_data().get("wait", True):
  672. result = task.get()
  673. project = pagure.lib.query._get_project(
  674. flask.g.session,
  675. name=result["repo"],
  676. namespace=result["namespace"],
  677. )
  678. output = {"message": 'Project "%s" created' % project.fullname}
  679. except pagure.exceptions.PagureException as err:
  680. raise pagure.exceptions.APIError(
  681. 400, error_code=APIERROR.ENOCODE, error=str(err)
  682. )
  683. except SQLAlchemyError as err: # pragma: no cover
  684. _log.exception(err)
  685. flask.g.session.rollback()
  686. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  687. else:
  688. raise pagure.exceptions.APIError(
  689. 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
  690. )
  691. jsonout = flask.jsonify(output)
  692. return jsonout
  693. @API.route("/<repo>", methods=["PATCH"])
  694. @API.route("/<namespace>/<repo>", methods=["PATCH"])
  695. @api_login_required(acls=["modify_project"])
  696. @api_method
  697. def api_modify_project(repo, namespace=None):
  698. """
  699. Modify a project
  700. ----------------
  701. Modify an existing project on this Pagure instance.
  702. ::
  703. PATCH /api/0/<repo>
  704. Input
  705. ^^^^^
  706. +------------------+---------+--------------+---------------------------+
  707. | Key | Type | Optionality | Description |
  708. +==================+=========+==============+===========================+
  709. | ``main_admin`` | string | Mandatory | | The new main admin of |
  710. | | | | the project. |
  711. +------------------+---------+--------------+---------------------------+
  712. | ``retain_access``| string | Optional | | The old main admin |
  713. | | | | retains access on the |
  714. | | | | project when giving the |
  715. | | | | project. Defaults to |
  716. | | | | ``False``. |
  717. +------------------+---------+--------------+---------------------------+
  718. Sample response
  719. ^^^^^^^^^^^^^^^
  720. ::
  721. {
  722. "access_groups": {
  723. "admin": [],
  724. "commit": [],
  725. "ticket": []
  726. },
  727. "access_users": {
  728. "admin": [],
  729. "commit": [],
  730. "owner": [
  731. "testuser1"
  732. ],
  733. "ticket": []
  734. },
  735. "close_status": [],
  736. "custom_keys": [],
  737. "date_created": "1496326387",
  738. "description": "Test",
  739. "fullname": "test-project2",
  740. "id": 2,
  741. "milestones": {},
  742. "name": "test-project2",
  743. "namespace": null,
  744. "parent": null,
  745. "priorities": {},
  746. "tags": [],
  747. "user": {
  748. "default_email": "testuser1@domain.local",
  749. "emails": [],
  750. "fullname": "Test User1",
  751. "name": "testuser1"
  752. }
  753. }
  754. """
  755. project = _get_repo(repo, namespace=namespace)
  756. _check_token(project, project_token=False)
  757. is_site_admin = pagure.utils.is_admin()
  758. admins = [u.username for u in project.get_project_users("admin")]
  759. # Only allow the main admin, the admins of the project, and Pagure site
  760. # admins to modify projects, even if the user has the right ACLs on their
  761. # token
  762. if (
  763. flask.g.fas_user.username not in admins
  764. and flask.g.fas_user.username != project.user.username
  765. and not is_site_admin
  766. ):
  767. raise pagure.exceptions.APIError(
  768. 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
  769. )
  770. valid_keys = ["main_admin", "retain_access"]
  771. # Check if it's JSON or form data
  772. if flask.request.headers.get("Content-Type") == "application/json":
  773. # Set force to True to ignore the mimetype. Set silent so that None is
  774. # returned if it's invalid JSON.
  775. args = flask.request.get_json(force=True, silent=True) or {}
  776. retain_access = args.get("retain_access", False)
  777. else:
  778. args = get_request_data()
  779. retain_access = args.get("retain_access", "").lower() in ["true", "1"]
  780. if not args:
  781. raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
  782. # Check to make sure there aren't parameters we don't support
  783. for key in args.keys():
  784. if key not in valid_keys:
  785. raise pagure.exceptions.APIError(
  786. 400, error_code=APIERROR.EINVALIDREQ
  787. )
  788. if "main_admin" in args:
  789. if (
  790. flask.g.fas_user.username != project.user.username
  791. and not is_site_admin
  792. ):
  793. raise pagure.exceptions.APIError(
  794. 401, error_code=APIERROR.ENOTMAINADMIN
  795. )
  796. # If the main_admin is already set correctly, don't do anything
  797. if flask.g.fas_user.username == project.user:
  798. return flask.jsonify(project.to_json(public=False, api=True))
  799. try:
  800. new_main_admin = pagure.lib.query.get_user(
  801. flask.g.session, args["main_admin"]
  802. )
  803. except pagure.exceptions.PagureException:
  804. raise pagure.exceptions.APIError(400, error_code=APIERROR.ENOUSER)
  805. old_main_admin = project.user.user
  806. pagure.lib.query.set_project_owner(
  807. flask.g.session, project, new_main_admin
  808. )
  809. if retain_access and flask.g.fas_user.username == old_main_admin:
  810. pagure.lib.query.add_user_to_project(
  811. flask.g.session,
  812. project,
  813. new_user=flask.g.fas_user.username,
  814. user=flask.g.fas_user.username,
  815. )
  816. try:
  817. flask.g.session.commit()
  818. except SQLAlchemyError: # pragma: no cover
  819. flask.g.session.rollback()
  820. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  821. pagure.lib.git.generate_gitolite_acls(project=project)
  822. return flask.jsonify(project.to_json(public=False, api=True))
  823. @API.route("/fork/", methods=["POST"])
  824. @API.route("/fork", methods=["POST"])
  825. @api_login_required(acls=["fork_project"])
  826. @api_method
  827. def api_fork_project():
  828. """
  829. Fork a project
  830. --------------------
  831. Fork a project on this pagure instance.
  832. This is an asynchronous call.
  833. ::
  834. POST /api/0/fork
  835. Input
  836. ^^^^^
  837. +------------------+---------+--------------+---------------------------+
  838. | Key | Type | Optionality | Description |
  839. +==================+=========+==============+===========================+
  840. | ``repo`` | string | Mandatory | | The name of the project |
  841. | | | | to fork. |
  842. +------------------+---------+--------------+---------------------------+
  843. | ``namespace`` | string | Optional | | The namespace of the |
  844. | | | | project to fork. |
  845. +------------------+---------+--------------+---------------------------+
  846. | ``username`` | string | Optional | | The username of the user|
  847. | | | | of the fork. |
  848. +------------------+---------+--------------+---------------------------+
  849. | ``wait`` | boolean | Optional | | A boolean to specify if |
  850. | | | | this API call should |
  851. | | | | return a taskid or if it|
  852. | | | | should wait for the task|
  853. | | | | to finish. |
  854. +------------------+---------+--------------+---------------------------+
  855. Sample response
  856. ^^^^^^^^^^^^^^^
  857. ::
  858. wait=False:
  859. {
  860. "message": "Project forking queued",
  861. "taskid": "123-abcd"
  862. }
  863. wait=True:
  864. {
  865. "message": 'Repo "test" cloned to "pingou/test"
  866. }
  867. """
  868. output = {}
  869. form = pagure.forms.ForkRepoForm(csrf_enabled=False)
  870. if form.validate_on_submit():
  871. repo = form.repo.data
  872. username = form.username.data or None
  873. namespace = form.namespace.data.strip() or None
  874. repo = get_authorized_api_project(
  875. flask.g.session, repo, user=username, namespace=namespace
  876. )
  877. if repo is None:
  878. raise pagure.exceptions.APIError(
  879. 404, error_code=APIERROR.ENOPROJECT
  880. )
  881. try:
  882. task = pagure.lib.query.fork_project(
  883. flask.g.session, user=flask.g.fas_user.username, repo=repo
  884. )
  885. flask.g.session.commit()
  886. output = {"message": "Project forking queued", "taskid": task.id}
  887. if get_request_data().get("wait", True):
  888. task.get()
  889. output = {
  890. "message": 'Repo "%s" cloned to "%s/%s"'
  891. % (repo.fullname, flask.g.fas_user.username, repo.fullname)
  892. }
  893. except pagure.exceptions.PagureException as err:
  894. raise pagure.exceptions.APIError(
  895. 400, error_code=APIERROR.ENOCODE, error=str(err)
  896. )
  897. except SQLAlchemyError as err: # pragma: no cover
  898. _log.exception(err)
  899. flask.g.session.rollback()
  900. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  901. else:
  902. raise pagure.exceptions.APIError(
  903. 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
  904. )
  905. jsonout = flask.jsonify(output)
  906. return jsonout
  907. @API.route("/<repo>/git/generateacls", methods=["POST"])
  908. @API.route("/<namespace>/<repo>/git/generateacls", methods=["POST"])
  909. @API.route("/fork/<username>/<repo>/git/generateacls", methods=["POST"])
  910. @API.route(
  911. "/fork/<username>/<namespace>/<repo>/git/generateacls", methods=["POST"]
  912. )
  913. @api_login_required(acls=["generate_acls_project"])
  914. @api_method
  915. def api_generate_acls(repo, username=None, namespace=None):
  916. """
  917. Generate Gitolite ACLs on a project
  918. -----------------------------------
  919. Generate Gitolite ACLs on a project. This is restricted to Pagure admins.
  920. This is an asynchronous call.
  921. ::
  922. POST /api/0/rpms/python-requests/git/generateacls
  923. Input
  924. ^^^^^
  925. +------------------+---------+--------------+---------------------------+
  926. | Key | Type | Optionality | Description |
  927. +==================+=========+==============+===========================+
  928. | ``wait`` | boolean | Optional | | A boolean to specify if |
  929. | | | | this API call should |
  930. | | | | return a taskid or if it|
  931. | | | | should wait for the task|
  932. | | | | to finish. |
  933. +------------------+---------+--------------+---------------------------+
  934. Sample response
  935. ^^^^^^^^^^^^^^^
  936. ::
  937. wait=False:
  938. {
  939. 'message': 'Project ACL generation queued',
  940. 'taskid': '123-abcd'
  941. }
  942. wait=True:
  943. {
  944. 'message': 'Project ACLs generated'
  945. }
  946. """
  947. project = _get_repo(repo, username, namespace)
  948. _check_token(project, project_token=False)
  949. # Check if it's JSON or form data
  950. if flask.request.headers.get("Content-Type") == "application/json":
  951. # Set force to True to ignore the mimetype. Set silent so that None is
  952. # returned if it's invalid JSON.
  953. json = flask.request.get_json(force=True, silent=True) or {}
  954. wait = json.get("wait", False)
  955. else:
  956. wait = pagure.utils.is_true(get_request_data().get("wait"))
  957. try:
  958. task = pagure.lib.git.generate_gitolite_acls(project=project)
  959. if wait:
  960. task.get()
  961. output = {"message": "Project ACLs generated"}
  962. else:
  963. output = {
  964. "message": "Project ACL generation queued",
  965. "taskid": task.id,
  966. }
  967. except pagure.exceptions.PagureException as err:
  968. raise pagure.exceptions.APIError(
  969. 400, error_code=APIERROR.ENOCODE, error=str(err)
  970. )
  971. jsonout = flask.jsonify(output)
  972. return jsonout
  973. @API.route("/<repo>/git/branch", methods=["POST"])
  974. @API.route("/<namespace>/<repo>/git/branch", methods=["POST"])
  975. @API.route("/fork/<username>/<repo>/git/branch", methods=["POST"])
  976. @API.route("/fork/<username>/<namespace>/<repo>/git/branch", methods=["POST"])
  977. @api_login_required(acls=["create_branch"])
  978. @api_method
  979. def api_new_branch(repo, username=None, namespace=None):
  980. """
  981. Create a new git branch on a project
  982. ------------------------------------
  983. Create a new git branch on a project
  984. ::
  985. POST /api/0/rpms/python-requests/git/branch
  986. Input
  987. ^^^^^
  988. +------------------+---------+--------------+---------------------------+
  989. | Key | Type | Optionality | Description |
  990. +==================+=========+==============+===========================+
  991. | ``branch`` | string | Mandatory | | A string of the branch |
  992. | | | | to create. |
  993. +------------------+---------+--------------+---------------------------+
  994. | ``from_branch`` | string | Optional | | A string of the branch |
  995. | | | | to branch off of. This |
  996. | | | | defaults to "master". |
  997. | | | | if ``from_commit`` |
  998. | | | | isn't set. |
  999. +------------------+---------+--------------+---------------------------+
  1000. | ``from_commit`` | string | Optional | | A string of the commit |
  1001. | | | | to branch off of. |
  1002. +------------------+---------+--------------+---------------------------+
  1003. Sample response
  1004. ^^^^^^^^^^^^^^^
  1005. ::
  1006. {
  1007. 'message': 'Project branch was created'
  1008. }
  1009. """
  1010. project = _get_repo(repo, username, namespace)
  1011. _check_token(project, project_token=False)
  1012. # Check if it's JSON or form data
  1013. if flask.request.headers.get("Content-Type") == "application/json":
  1014. # Set force to True to ignore the mimetype. Set silent so that None is
  1015. # returned if it's invalid JSON.
  1016. args = flask.request.get_json(force=True, silent=True) or {}
  1017. else:
  1018. args = get_request_data()
  1019. branch = args.get("branch")
  1020. from_branch = args.get("from_branch")
  1021. from_commit = args.get("from_commit")
  1022. if from_branch and from_commit:
  1023. raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
  1024. if (
  1025. not branch
  1026. or not isinstance(branch, string_types)
  1027. or (from_branch and not isinstance(from_branch, string_types))
  1028. or (from_commit and not isinstance(from_commit, string_types))
  1029. ):
  1030. raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
  1031. try:
  1032. pagure.lib.git.new_git_branch(
  1033. flask.g.fas_user.username,
  1034. project,
  1035. branch,
  1036. from_branch=from_branch,
  1037. from_commit=from_commit,
  1038. )
  1039. except GitError: # pragma: no cover
  1040. raise pagure.exceptions.APIError(400, error_code=APIERROR.EGITERROR)
  1041. except pagure.exceptions.PagureException as error:
  1042. raise pagure.exceptions.APIError(
  1043. 400, error_code=APIERROR.ENOCODE, error=str(error)
  1044. )
  1045. output = {"message": "Project branch was created"}
  1046. jsonout = flask.jsonify(output)
  1047. return jsonout
  1048. @API.route("/<repo>/c/<commit_hash>/flag")
  1049. @API.route("/<namespace>/<repo>/c/<commit_hash>/flag")
  1050. @API.route("/fork/<username>/<repo>/c/<commit_hash>/flag")
  1051. @API.route("/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag")
  1052. @api_method
  1053. def api_commit_flags(repo, commit_hash, username=None, namespace=None):
  1054. """
  1055. Flags for a commit
  1056. ------------------
  1057. Return all flags for given commit of given project
  1058. ::
  1059. GET /api/0/<repo>/c/<commit_hash>/flag
  1060. GET /api/0/<namespace>/<repo>/c/<commit_hash>/flag
  1061. ::
  1062. GET /api/0/fork/<username>/<repo>/c/<commit_hash>/flag
  1063. GET /api/0/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag
  1064. Sample response
  1065. ^^^^^^^^^^^^^^^
  1066. ::
  1067. {
  1068. "flags": [
  1069. {
  1070. "comment": "flag-comment",
  1071. "commit_hash": "28f1f7fe844301f0e5f7aecacae0a1e5ec50a090",
  1072. "date_created": "1520341983",
  1073. "percent": null,
  1074. "status": "success",
  1075. "url": "https://some.url.com",
  1076. "user": {
  1077. "fullname": "Full name",
  1078. "name": "fname"
  1079. },
  1080. "username": "somename"
  1081. },
  1082. {
  1083. "comment": "different-comment",
  1084. "commit_hash": "28f1f7fe844301f0e5f7aecacae0a1e5ec50a090",
  1085. "date_created": "1520512543",
  1086. "percent": null,
  1087. "status": "pending",
  1088. "url": "https://other.url.com",
  1089. "user": {
  1090. "fullname": "Other Name",
  1091. "name": "oname"
  1092. },
  1093. "username": "differentname"
  1094. }
  1095. ],
  1096. "total_flags": 2
  1097. }
  1098. """
  1099. repo = _get_repo(repo, username, namespace)
  1100. reponame = pagure.utils.get_repo_path(repo)
  1101. repo_obj = Repository(reponame)
  1102. try:
  1103. repo_obj.get(commit_hash)
  1104. except ValueError:
  1105. raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOCOMMIT)
  1106. flags = pagure.lib.query.get_commit_flag(
  1107. flask.g.session, repo, commit_hash
  1108. )
  1109. flags = [f.to_json(public=True) for f in flags]
  1110. return flask.jsonify({"total_flags": len(flags), "flags": flags})
  1111. @API.route("/<repo>/c/<commit_hash>/flag", methods=["POST"])
  1112. @API.route("/<namespace>/<repo>/c/<commit_hash>/flag", methods=["POST"])
  1113. @API.route("/fork/<username>/<repo>/c/<commit_hash>/flag", methods=["POST"])
  1114. @API.route(
  1115. "/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag",
  1116. methods=["POST"],
  1117. )
  1118. @api_login_required(acls=["commit_flag"])
  1119. @api_method
  1120. def api_commit_add_flag(repo, commit_hash, username=None, namespace=None):
  1121. """
  1122. Flag a commit
  1123. -------------------
  1124. Add or edit flags on a commit.
  1125. ::
  1126. POST /api/0/<repo>/c/<commit_hash>/flag
  1127. POST /api/0/<namespace>/<repo>/c/<commit_hash>/flag
  1128. ::
  1129. POST /api/0/fork/<username>/<repo>/c/<commit_hash>/flag
  1130. POST /api/0/fork/<username>/<namespace>/<repo>/c/<commit_hash>/flag
  1131. Input
  1132. ^^^^^
  1133. +---------------+---------+--------------+-----------------------------+
  1134. | Key | Type | Optionality | Description |
  1135. +===============+=========+==============+=============================+
  1136. | ``username`` | string | Mandatory | | The name of the |
  1137. | | | | application to be |
  1138. | | | | presented to users |
  1139. | | | | on the commit pages |
  1140. +---------------+---------+--------------+-----------------------------+
  1141. | ``comment`` | string | Mandatory | | A short message |
  1142. | | | | summarizing the |
  1143. | | | | presented results |
  1144. +---------------+---------+--------------+-----------------------------+
  1145. | ``url`` | string | Mandatory | | A URL to the result |
  1146. | | | | of this flag |
  1147. +---------------+---------+--------------+-----------------------------+
  1148. | ``status`` | string | Mandatory | | The status of the task, |
  1149. | | | | can be any of: |
  1150. | | | | $$FLAG_STATUSES_COMMAS$$ |
  1151. +---------------+---------+--------------+-----------------------------+
  1152. | ``percent`` | int | Optional | | A percentage of |
  1153. | | | | completion compared to |
  1154. | | | | the goal. The percentage |
  1155. | | | | also determine the |
  1156. | | | | background color of the |
  1157. | | | | flag on the pages |
  1158. +---------------+---------+--------------+-----------------------------+
  1159. | ``uid`` | string | Optional | | A unique identifier used |
  1160. | | | | to identify a flag across |
  1161. | | | | all projects. If the |
  1162. | | | | provided UID matches an |
  1163. | | | | existing one, then the |
  1164. | | | | API call will update the |
  1165. | | | | existing one rather than |
  1166. | | | | create a new one. |
  1167. | | | | Maximum Length: 32 |
  1168. | | | | characters. Default: an |
  1169. | | | | auto generated UID |
  1170. +---------------+---------+--------------+-----------------------------+
  1171. Sample response
  1172. ^^^^^^^^^^^^^^^
  1173. ::
  1174. {
  1175. "flag": {
  1176. "comment": "Tests passed",
  1177. "commit_hash": "62b49f00d489452994de5010565fab81",
  1178. "date_created": "1510742565",
  1179. "percent": 100,
  1180. "status": "success",
  1181. "url": "http://jenkins.cloud.fedoraproject.org/",
  1182. "user": {
  1183. "default_email": "bar@pingou.com",
  1184. "emails": ["bar@pingou.com", "foo@pingou.com"],
  1185. "fullname": "PY C",
  1186. "name": "pingou"},
  1187. "username": "Jenkins"
  1188. },
  1189. "message": "Flag added",
  1190. "uid": "b1de8f80defd4a81afe2e09f39678087"
  1191. }
  1192. ::
  1193. {
  1194. "flag": {
  1195. "comment": "Tests passed",
  1196. "commit_hash": "62b49f00d489452994de5010565fab81",
  1197. "date_created": "1510742565",
  1198. "percent": 100,
  1199. "status": "success",
  1200. "url": "http://jenkins.cloud.fedoraproject.org/",
  1201. "user": {
  1202. "default_email": "bar@pingou.com",
  1203. "emails": ["bar@pingou.com", "foo@pingou.com"],
  1204. "fullname": "PY C",
  1205. "name": "pingou"},
  1206. "username": "Jenkins"
  1207. },
  1208. "message": "Flag updated",
  1209. "uid": "b1de8f80defd4a81afe2e09f39678087"
  1210. }
  1211. """ # noqa
  1212. repo = _get_repo(repo, username, namespace)
  1213. _check_token(repo, project_token=False)
  1214. output = {}
  1215. reponame = pagure.utils.get_repo_path(repo)
  1216. repo_obj = Repository(reponame)
  1217. try:
  1218. repo_obj.get(commit_hash)
  1219. except ValueError:
  1220. raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOCOMMIT)
  1221. form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
  1222. if form.validate_on_submit():
  1223. username = form.username.data
  1224. percent = form.percent.data.strip() or None
  1225. comment = form.comment.data.strip()
  1226. url = form.url.data.strip()
  1227. uid = form.uid.data.strip() if form.uid.data else None
  1228. status = form.status.data.strip()
  1229. try:
  1230. # New Flag
  1231. message, uid = pagure.lib.query.add_commit_flag(
  1232. session=flask.g.session,
  1233. repo=repo,
  1234. commit_hash=commit_hash,
  1235. username=username,
  1236. percent=percent,
  1237. comment=comment,
  1238. status=status,
  1239. url=url,
  1240. uid=uid,
  1241. user=flask.g.fas_user.username,
  1242. token=flask.g.token.id,
  1243. )
  1244. flask.g.session.commit()
  1245. c_flag = pagure.lib.query.get_commit_flag_by_uid(
  1246. flask.g.session, commit_hash, uid
  1247. )
  1248. output["message"] = message
  1249. output["uid"] = uid
  1250. output["flag"] = c_flag.to_json()
  1251. except pagure.exceptions.PagureException as err:
  1252. raise pagure.exceptions.APIError(
  1253. 400, error_code=APIERROR.ENOCODE, error=str(err)
  1254. )
  1255. except SQLAlchemyError as err: # pragma: no cover
  1256. flask.g.session.rollback()
  1257. _log.exception(err)
  1258. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  1259. else:
  1260. raise pagure.exceptions.APIError(
  1261. 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
  1262. )
  1263. jsonout = flask.jsonify(output)
  1264. return jsonout
  1265. @API.route("/<repo>/watchers/update", methods=["POST"])
  1266. @API.route("/<namespace>/<repo>/watchers/update", methods=["POST"])
  1267. @API.route("/fork/<username>/<repo>/watchers/update", methods=["POST"])
  1268. @API.route(
  1269. "/fork/<username>/<namespace>/<repo>/watchers/update", methods=["POST"]
  1270. )
  1271. @api_login_required(acls=["update_watch_status"])
  1272. @api_method
  1273. def api_update_project_watchers(repo, username=None, namespace=None):
  1274. """
  1275. Update project watchers
  1276. -----------------------
  1277. Allows anyone to update their own watch status on the project.
  1278. ::
  1279. POST /api/0/<repo>/watchers/update
  1280. POST /api/0/<namespace>/<repo>/watchers/update
  1281. ::
  1282. POST /api/0/fork/<username>/<repo>/watchers/update
  1283. POST /api/0/fork/<username>/<namespace>/<repo>/watchers/update
  1284. Input
  1285. ^^^^^
  1286. +------------------+---------+--------------+---------------------------+
  1287. | Key | Type | Optionality | Description |
  1288. +==================+=========+==============+===========================+
  1289. | ``repo`` | string | Mandatory | | The name of the project |
  1290. | | | | to fork. |
  1291. +------------------+---------+--------------+---------------------------+
  1292. | ``status`` | string | Mandatory | | The new watch status to |
  1293. | | | | set on that project. |
  1294. | | | | (See options below) |
  1295. +------------------+---------+--------------+---------------------------+
  1296. | ``watcher`` | string | Mandatory | | The name of the user |
  1297. | | | | changing their watch |
  1298. | | | | status. |
  1299. +------------------+---------+--------------+---------------------------+
  1300. | ``namespace`` | string | Optional | | The namespace of the |
  1301. | | | | project to fork. |
  1302. +------------------+---------+--------------+---------------------------+
  1303. | ``username`` | string | Optional | | The username of the user|
  1304. | | | | of the fork. |
  1305. +------------------+---------+--------------+---------------------------+
  1306. Watch Status
  1307. ^^^^^^^^^^^^
  1308. +------------+----------------------------------------------+
  1309. | Key | Description |
  1310. +============+==============================================+
  1311. | -1 | Reset the watch status to default |
  1312. +------------+----------------------------------------------+
  1313. | 0 | Unwatch, don't notify the user of anything |
  1314. +------------+----------------------------------------------+
  1315. | 1 | Watch issues and pull-requests |
  1316. +------------+----------------------------------------------+
  1317. | 2 | Watch commits |
  1318. +------------+----------------------------------------------+
  1319. | 3 | Watch commits, issues and pull-requests |
  1320. +------------+----------------------------------------------+
  1321. Sample response
  1322. ^^^^^^^^^^^^^^^
  1323. ::
  1324. {
  1325. "message": "You are now watching issues and PRs on this project",
  1326. "status": "ok"
  1327. }
  1328. """
  1329. project = _get_repo(repo, username, namespace)
  1330. _check_token(project)
  1331. # Get the input submitted
  1332. data = get_request_data()
  1333. watcher = data.get("watcher")
  1334. if not watcher:
  1335. _log.debug("api_update_project_watchers: Invalid watcher: %s", watcher)
  1336. raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
  1337. is_site_admin = pagure.utils.is_admin()
  1338. # Only allow the main admin, and the user themselves to update their
  1339. # status
  1340. if not is_site_admin and flask.g.fas_user.username != watcher:
  1341. raise pagure.exceptions.APIError(
  1342. 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
  1343. )
  1344. try:
  1345. pagure.lib.query.get_user(flask.g.session, watcher)
  1346. except pagure.exceptions.PagureException:
  1347. _log.debug(
  1348. "api_update_project_watchers: Invalid user watching: %s", watcher
  1349. )
  1350. raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
  1351. watch_status = data.get("status")
  1352. try:
  1353. msg = pagure.lib.query.update_watch_status(
  1354. session=flask.g.session,
  1355. project=project,
  1356. user=watcher,
  1357. watch=watch_status,
  1358. )
  1359. flask.g.session.commit()
  1360. except pagure.exceptions.PagureException as err:
  1361. raise pagure.exceptions.APIError(
  1362. 400, error_code=APIERROR.ENOCODE, error=str(err)
  1363. )
  1364. except SQLAlchemyError as err: # pragma: no cover
  1365. flask.g.session.rollback()
  1366. _log.exception(err)
  1367. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  1368. return flask.jsonify({"message": msg, "status": "ok"})
  1369. @API.route("/<repo>/git/modifyacls", methods=["POST"])
  1370. @API.route("/<namespace>/<repo>/git/modifyacls", methods=["POST"])
  1371. @API.route("/fork/<username>/<repo>/git/modifyacls", methods=["POST"])
  1372. @API.route(
  1373. "/fork/<username>/<namespace>/<repo>/git/modifyacls", methods=["POST"]
  1374. )
  1375. @api_login_required(acls=["modify_project"])
  1376. @api_method
  1377. def api_modify_acls(repo, namespace=None, username=None):
  1378. """
  1379. Modify ACLs on a project
  1380. ------------------------
  1381. Add, remove or update ACLs on a project for a particular user or group.
  1382. This is restricted to project admins.
  1383. ::
  1384. POST /api/0/<repo>/git/modifyacls
  1385. POST /api/0/<namespace>/<repo>/git/modifyacls
  1386. ::
  1387. POST /api/0/fork/<username>/<repo>/git/modifyacls
  1388. POST /api/0/fork/<username>/<namespace>/<repo>/git/modifyacls
  1389. Input
  1390. ^^^^^
  1391. +------------------+---------+---------------+---------------------------+
  1392. | Key | Type | Optionality | Description |
  1393. +==================+=========+===============+===========================+
  1394. | ``user_type`` | String | Mandatory | A string to specify if |
  1395. | | | | the ACL should be changed |
  1396. | | | | for a user or a group. |
  1397. | | | | Specifying one of either |
  1398. | | | | 'user' or 'group' is |
  1399. | | | | mandatory |
  1400. | | | | |
  1401. +------------------+---------+---------------+---------------------------+
  1402. | ``name`` | String | Mandatory | The name of the user or |
  1403. | | | | group whose ACL |
  1404. | | | | should be changed. |
  1405. | | | | |
  1406. +------------------+---------+---------------+---------------------------+
  1407. | ``acl`` | String | Optional | Can be either unspecified,|
  1408. | | | | 'ticket', 'commit', |
  1409. | | | | 'admin'. If unspecified, |
  1410. | | | | the access will be removed|
  1411. | | | | |
  1412. +------------------+---------+---------------+---------------------------+
  1413. Sample response
  1414. ^^^^^^^^^^^^^^^
  1415. ::
  1416. {
  1417. "access_groups": {
  1418. "admin": [],
  1419. "commit": [],
  1420. "ticket": []
  1421. },
  1422. "access_users": {
  1423. "admin": [],
  1424. "commit": [
  1425. "ta2"
  1426. ],
  1427. "owner": [
  1428. "karsten"
  1429. ],
  1430. "ticket": [
  1431. "ta1"
  1432. ]
  1433. },
  1434. "close_status": [],
  1435. "custom_keys": [],
  1436. "date_created": "1531131619",
  1437. "date_modified": "1531302337",
  1438. "description": "pagure local instance",
  1439. "fullname": "pagure",
  1440. "id": 1,
  1441. "milestones": {},
  1442. "name": "pagure",
  1443. "namespace": null,
  1444. "parent": null,
  1445. "priorities": {},
  1446. "tags": [],
  1447. "url_path": "pagure",
  1448. "user": {
  1449. "fullname": "KH",
  1450. "name": "karsten"
  1451. }
  1452. }
  1453. """
  1454. output = {}
  1455. project = _get_repo(repo, username, namespace)
  1456. _check_token(project, project_token=False)
  1457. form = pagure.forms.ModifyACLForm(csrf_enabled=False)
  1458. if form.validate_on_submit():
  1459. acl = form.acl.data
  1460. group = None
  1461. user = None
  1462. if form.user_type.data == "user":
  1463. user = form.name.data
  1464. else:
  1465. group = form.name.data
  1466. is_site_admin = pagure.utils.is_admin()
  1467. admins = [u.username for u in project.get_project_users("admin")]
  1468. if not acl:
  1469. if (
  1470. user
  1471. and flask.g.fas_user.username != user
  1472. and flask.g.fas_user.username not in admins
  1473. and flask.g.fas_user.username != project.user.username
  1474. and not is_site_admin
  1475. ):
  1476. raise pagure.exceptions.APIError(
  1477. 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
  1478. )
  1479. elif (
  1480. flask.g.fas_user.username not in admins
  1481. and flask.g.fas_user.username != project.user.username
  1482. and not is_site_admin
  1483. ):
  1484. raise pagure.exceptions.APIError(
  1485. 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
  1486. )
  1487. if user:
  1488. user_obj = pagure.lib.query.search_user(
  1489. flask.g.session, username=user
  1490. )
  1491. if not user_obj:
  1492. raise pagure.exceptions.APIError(
  1493. 404, error_code=APIERROR.ENOUSER
  1494. )
  1495. elif group:
  1496. group_obj = pagure.lib.query.search_groups(
  1497. flask.g.session, group_name=group
  1498. )
  1499. if not group_obj:
  1500. raise pagure.exceptions.APIError(
  1501. 404, error_code=APIERROR.ENOGROUP
  1502. )
  1503. if acl:
  1504. if (
  1505. user
  1506. and user_obj not in project.access_users[acl]
  1507. and user_obj.user != project.user.user
  1508. ):
  1509. _log.info(
  1510. "Adding user %s to project: %s", user, project.fullname
  1511. )
  1512. pagure.lib.query.add_user_to_project(
  1513. session=flask.g.session,
  1514. project=project,
  1515. new_user=user,
  1516. user=flask.g.fas_user.username,
  1517. access=acl,
  1518. )
  1519. elif group and group_obj not in project.access_groups[acl]:
  1520. _log.info(
  1521. "Adding group %s to project: %s", group, project.fullname
  1522. )
  1523. pagure.lib.query.add_group_to_project(
  1524. session=flask.g.session,
  1525. project=project,
  1526. new_group=group,
  1527. user=flask.g.fas_user.username,
  1528. access=acl,
  1529. create=pagure_config.get("ENABLE_GROUP_MNGT", False),
  1530. is_admin=pagure.utils.is_admin(),
  1531. )
  1532. else:
  1533. if user:
  1534. _log.info(
  1535. "Looking at removing user %s from project %s",
  1536. user,
  1537. project.fullname,
  1538. )
  1539. try:
  1540. pagure.lib.query.remove_user_of_project(
  1541. flask.g.session,
  1542. user_obj,
  1543. project,
  1544. flask.g.fas_user.username,
  1545. )
  1546. except pagure.exceptions.PagureException as err:
  1547. raise pagure.exceptions.APIError(
  1548. 400, error_code=APIERROR.EINVALIDREQ, errors="%s" % err
  1549. )
  1550. elif group:
  1551. pass
  1552. try:
  1553. flask.g.session.commit()
  1554. except pagure.exceptions.PagureException as msg:
  1555. flask.g.session.rollback()
  1556. _log.debug(msg)
  1557. flask.flash(str(msg), "error")
  1558. except SQLAlchemyError as err:
  1559. _log.exception(err)
  1560. flask.g.session.rollback()
  1561. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  1562. pagure.lib.git.generate_gitolite_acls(project=project)
  1563. output = project.to_json(api=True, public=True)
  1564. else:
  1565. raise pagure.exceptions.APIError(
  1566. 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
  1567. )
  1568. jsonout = flask.jsonify(output)
  1569. return jsonout
  1570. @API.route("/<repo>/options", methods=["GET"])
  1571. @API.route("/<namespace>/<repo>/options", methods=["GET"])
  1572. @API.route("/fork/<username>/<repo>/options", methods=["GET"])
  1573. @API.route("/fork/<username>/<namespace>/<repo>/options", methods=["GET"])
  1574. @api_login_required(acls=["modify_project"])
  1575. @api_method
  1576. def api_get_project_options(repo, username=None, namespace=None):
  1577. """
  1578. Get project options
  1579. ----------------------
  1580. Allow project admins to retrieve the current options of a project.
  1581. ::
  1582. GET /api/0/<repo>/options
  1583. GET /api/0/<namespace>/<repo>/options
  1584. ::
  1585. GET /api/0/fork/<username>/<repo>/options
  1586. GET /api/0/fork/<username>/<namespace>/<repo>/options
  1587. Sample response
  1588. ^^^^^^^^^^^^^^^
  1589. ::
  1590. {
  1591. "settings": {
  1592. "Enforce_signed-off_commits_in_pull-request": false,
  1593. "Minimum_score_to_merge_pull-request": -1,
  1594. "Only_assignee_can_merge_pull-request": false,
  1595. "Web-hooks": null,
  1596. "always_merge": false,
  1597. "disable_non_fast-forward_merges": false,
  1598. "fedmsg_notifications": true,
  1599. "issue_tracker": true,
  1600. "issue_tracker_read_only": false,
  1601. "issues_default_to_private": false,
  1602. "notify_on_commit_flag": false,
  1603. "notify_on_pull-request_flag": false,
  1604. "open_metadata_access_to_all": false,
  1605. "project_documentation": false,
  1606. "pull_request_access_only": false,
  1607. "pull_requests": true,
  1608. "stomp_notifications": true
  1609. },
  1610. "status": "ok"
  1611. }
  1612. """
  1613. project = _get_repo(repo, username, namespace)
  1614. _check_token(project, project_token=False)
  1615. return flask.jsonify({"settings": project.settings, "status": "ok"})
  1616. @API.route("/<repo>/connector", methods=["GET"])
  1617. @API.route("/<namespace>/<repo>/connector", methods=["GET"])
  1618. @API.route("/fork/<username>/<repo>/connector", methods=["GET"])
  1619. @API.route("/fork/<username>/<namespace>/<repo>/connector", methods=["GET"])
  1620. @api_login_required(acls=["modify_project"])
  1621. @api_method
  1622. def api_get_project_connector(repo, username=None, namespace=None):
  1623. """
  1624. Get project connector
  1625. ---------------------
  1626. Allow project owners and admins to retrieve their own connector tokens.
  1627. Connector tokens are the API tokens and the Web Hook token
  1628. of the project. Connector tokens make possible for an external
  1629. application to listen and verify project notifications and act
  1630. on project via the REST API.
  1631. ::
  1632. GET /api/0/<repo>/connector
  1633. GET /api/0/<namespace>/<repo>/connector
  1634. ::
  1635. GET /api/0/fork/<username>/<repo>/connector
  1636. GET /api/0/fork/<username>/<namespace>/<repo>/connector
  1637. Sample response
  1638. ^^^^^^^^^^^^^^^
  1639. ::
  1640. {
  1641. "connector": {
  1642. "hook_token": "aaabbbccc",
  1643. "api_token": [
  1644. {'name': 'foo token',
  1645. 'id': "abcdefoo",
  1646. 'expired': True}
  1647. {'name': 'bar token',
  1648. 'id': "abcdebar",
  1649. 'expired': False}
  1650. ]
  1651. },
  1652. "status": "ok"
  1653. }
  1654. """
  1655. project = _get_repo(repo, username, namespace)
  1656. _check_token(project, project_token=False)
  1657. authorized_users = [project.user.username]
  1658. authorized_users.extend(
  1659. [user.user for user in project.access_users["admin"]]
  1660. )
  1661. if flask.g.fas_user.user not in authorized_users:
  1662. raise pagure.exceptions.APIError(
  1663. 401, error_code=APIERROR.ENOTHIGHENOUGH
  1664. )
  1665. user_obj = pagure.lib.query.search_user(
  1666. flask.g.session, username=flask.g.fas_user.user
  1667. )
  1668. user_project_tokens = [
  1669. token for token in user_obj.tokens if token.project_id == project.id
  1670. ]
  1671. connector = {
  1672. "hook_token": project.hook_token,
  1673. "api_tokens": [
  1674. {"description": t.description, "id": t.id, "expired": t.expired}
  1675. for t in user_project_tokens
  1676. ],
  1677. }
  1678. return flask.jsonify({"connector": connector, "status": "ok"})
  1679. def _check_value(value):
  1680. """ Convert the provided value into a boolean, an int or leave it as it.
  1681. """
  1682. if str(value).lower() in ["true"]:
  1683. value = True
  1684. elif str(value).lower() in ["false"]:
  1685. value = True
  1686. elif str(value).isnumeric():
  1687. value = int(value)
  1688. return value
  1689. @API.route("/<repo>/options/update", methods=["POST"])
  1690. @API.route("/<namespace>/<repo>/options/update", methods=["POST"])
  1691. @API.route("/fork/<username>/<repo>/options/update", methods=["POST"])
  1692. @API.route(
  1693. "/fork/<username>/<namespace>/<repo>/options/update", methods=["POST"]
  1694. )
  1695. @api_login_required(acls=["modify_project"])
  1696. @api_method
  1697. def api_modify_project_options(repo, username=None, namespace=None):
  1698. """
  1699. Update project options
  1700. ----------------------
  1701. Allow project admins to modify the options of a project.
  1702. ::
  1703. POST /api/0/<repo>/options/update
  1704. POST /api/0/<namespace>/<repo>/options/update
  1705. ::
  1706. POST /api/0/fork/<username>/<repo>/options/update
  1707. POST /api/0/fork/<username>/<namespace>/<repo>/options/update
  1708. Input
  1709. ^^^^^
  1710. Simply specify the key/values you would like to set. Beware that if you
  1711. do not specify in the request values that have been changed before they
  1712. will go back to their default value.
  1713. Sample response
  1714. ^^^^^^^^^^^^^^^
  1715. ::
  1716. {
  1717. 'message': 'Edited successfully settings of repo: test',
  1718. 'status': 'ok'
  1719. }
  1720. """
  1721. project = _get_repo(repo, username, namespace)
  1722. _check_token(project, project_token=False)
  1723. settings = {}
  1724. for key in flask.request.form:
  1725. settings[key] = _check_value(flask.request.form[key])
  1726. try:
  1727. message = pagure.lib.query.update_project_settings(
  1728. flask.g.session,
  1729. repo=project,
  1730. settings=settings,
  1731. user=flask.g.fas_user.username,
  1732. from_api=True,
  1733. )
  1734. flask.g.session.commit()
  1735. except pagure.exceptions.PagureException as err:
  1736. raise pagure.exceptions.APIError(
  1737. 400, error_code=APIERROR.ENOCODE, error=str(err)
  1738. )
  1739. except SQLAlchemyError as err: # pragma: no cover
  1740. flask.g.session.rollback()
  1741. _log.exception(err)
  1742. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  1743. return flask.jsonify({"message": message, "status": "ok"})
  1744. @API.route("/<repo>/token/new", methods=["POST"])
  1745. @API.route("/<namespace>/<repo>/token/new", methods=["POST"])
  1746. @API.route("/fork/<username>/<repo>/token/new", methods=["POST"])
  1747. @API.route("/fork/<username>/<namespace>/<repo>/token/new", methods=["POST"])
  1748. @api_login_required(acls=["modify_project"])
  1749. @api_method
  1750. def api_project_create_api_token(repo, namespace=None, username=None):
  1751. """
  1752. Create API project Token
  1753. ------------------------
  1754. Create a project token API for the caller user
  1755. This is restricted to project admins.
  1756. ::
  1757. POST /api/0/<repo>/token/new
  1758. POST /api/0/<namespace>/<repo>/token/new
  1759. ::
  1760. POST /api/0/fork/<username>/<repo>/token/new
  1761. POST /api/0/fork/<username>/<namespace>/<repo>/token/new
  1762. Input
  1763. ^^^^^
  1764. +------------------+---------+---------------+---------------------------+
  1765. | Key | Type | Optionality | Description |
  1766. +==================+=========+===============+===========================+
  1767. | ``description`` | String | optional | A string to specify the |
  1768. | | | | description of the token |
  1769. | | | | |
  1770. +------------------+---------+---------------+---------------------------+
  1771. | ``acls`` | List | Mandatory | The ACLs |
  1772. | | | | |
  1773. +------------------+---------+---------------+---------------------------+
  1774. Sample response
  1775. ^^^^^^^^^^^^^^^
  1776. ::
  1777. {
  1778. "token": {
  1779. "description": "My foo token",
  1780. "id": "aaabbbcccfootoken",
  1781. },
  1782. }
  1783. """
  1784. output = {}
  1785. project = _get_repo(repo, username, namespace)
  1786. _check_token(project, project_token=False)
  1787. authorized_users = [project.user.username]
  1788. authorized_users.extend(
  1789. [user.user for user in project.access_users["admin"]]
  1790. )
  1791. if flask.g.fas_user.user not in authorized_users:
  1792. raise pagure.exceptions.APIError(
  1793. 401, error_code=APIERROR.ENOTHIGHENOUGH
  1794. )
  1795. authorized_acls = pagure_config.get("USER_ACLS", [])
  1796. form = pagure.forms.NewTokenForm(csrf_enabled=False, sacls=authorized_acls)
  1797. if form.validate_on_submit():
  1798. acls = form.acls.data
  1799. description = form.description.data
  1800. else:
  1801. raise pagure.exceptions.APIError(
  1802. 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
  1803. )
  1804. token = pagure.lib.query.add_token_to_user(
  1805. flask.g.session, project, acls, flask.g.fas_user.user, description
  1806. )
  1807. output = {"token": {"description": token.description, "id": token.id}}
  1808. jsonout = flask.jsonify(output)
  1809. return jsonout
  1810. @API.route("/<repo>/blockuser", methods=["POST"])
  1811. @API.route("/<namespace>/<repo>/blockuser", methods=["POST"])
  1812. @API.route("/fork/<username>/<repo>/blockuser", methods=["POST"])
  1813. @API.route("/fork/<username>/<namespace>/<repo>/blockuser", methods=["POST"])
  1814. @api_login_required(acls=["modify_project"])
  1815. @api_method
  1816. def api_project_block_user(repo, namespace=None, username=None):
  1817. """
  1818. Block an user from a project
  1819. ----------------------------
  1820. Block an user from interacting with the project
  1821. This is restricted to project admins.
  1822. ::
  1823. POST /api/0/<repo>/blockuser
  1824. POST /api/0/<namespace>/<repo>/blockuser
  1825. ::
  1826. POST /api/0/fork/<username>/<repo>/blockuser
  1827. POST /api/0/fork/<username>/<namespace>/<repo>/blockuser
  1828. Input
  1829. ^^^^^
  1830. +------------------+---------+---------------+---------------------------+
  1831. | Key | Type | Optionality | Description |
  1832. +==================+=========+===============+===========================+
  1833. | ``username`` | String | optional | The username of the user |
  1834. | | | | to block on this project |
  1835. +------------------+---------+---------------+---------------------------+
  1836. Beware that this API endpoint updates **all** the users blocked in the
  1837. project, so if you are updating this list, do not submit just one username,
  1838. submit the updated list.
  1839. Sample response
  1840. ^^^^^^^^^^^^^^^
  1841. ::
  1842. {"message": "User(s) blocked"}
  1843. """
  1844. output = {}
  1845. project = _get_repo(repo, username, namespace)
  1846. _check_token(project)
  1847. authorized_users = [project.user.username]
  1848. authorized_users.extend(
  1849. [user.user for user in project.access_users["admin"]]
  1850. )
  1851. if flask.g.fas_user.username not in authorized_users:
  1852. raise pagure.exceptions.APIError(
  1853. 401, error_code=APIERROR.ENOTHIGHENOUGH
  1854. )
  1855. usernames = flask.request.form.getlist("username")
  1856. try:
  1857. users = set()
  1858. for user in usernames:
  1859. user = user.strip()
  1860. if user:
  1861. pagure.lib.query.get_user(flask.g.session, user)
  1862. users.add(user)
  1863. project.block_users = list(users)
  1864. flask.g.session.add(project)
  1865. flask.g.session.commit()
  1866. output = {"message": "User(s) blocked"}
  1867. except pagure.exceptions.PagureException as err:
  1868. raise pagure.exceptions.APIError(
  1869. 400, error_code=APIERROR.ENOCODE, error=str(err)
  1870. )
  1871. except SQLAlchemyError as err: # pragma: no cover
  1872. flask.g.session.rollback()
  1873. _log.exception(err)
  1874. raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
  1875. jsonout = flask.jsonify(output)
  1876. return jsonout