comparison doc/rest.txt @ 7801:af898d1d66dc

doc: run sphinx-lint over docs. Pointed out mutiple use of `x` where it should be ``x``. Also trailing whitespace and lines that are too long. Replaced all tabs by spaces. Also fixed spelling error while I was there. Fixed broken internal link.
author John Rouillard <rouilj@ieee.org>
date Wed, 13 Mar 2024 00:51:09 -0400
parents 835b248bf9fd
children be6cb2e0d471
comparison
equal deleted inserted replaced
7800:2d4684e4702d 7801:af898d1d66dc
1 .. meta:: 1 .. meta::
2 :description: 2 :description:
3 Documentation on the RESTful interface to the Roundup Issue 3 Documentation on the RESTful interface to the Roundup Issue
4 Tracker. Enable REST access, endpoints, methods, 4 Tracker. Enable REST access, endpoints, methods,
5 authentication, discovery. 5 authentication, discovery.
6 6
7 .. index:: pair: api; Representational state transfer 7 .. index:: pair: api; Representational state transfer
8 pair: api; rest 8 pair: api; rest
9 9
257 257
258 CORS preflight requests are done using the OPTIONS method. They 258 CORS preflight requests are done using the OPTIONS method. They
259 require that REST be enabled. These requests do not make any changes 259 require that REST be enabled. These requests do not make any changes
260 or get any information from the database. As a result they are 260 or get any information from the database. As a result they are
261 available to the anonymous user and any authenticated user. The user 261 available to the anonymous user and any authenticated user. The user
262 does not need to have `Rest Access` permissions. Also these requests 262 does not need to have ``Rest Access`` permissions. Also these requests
263 bypass CSRF checks except for the Origin header check which is always 263 bypass CSRF checks except for the Origin header check which is always
264 run for preflight requests. 264 run for preflight requests.
265 265
266 You can permit only allowed ORIGINS by setting ``allowed_api_origins`` 266 You can permit only allowed ORIGINS by setting ``allowed_api_origins``
267 in ``config.ini`` to the list of origins permitted to access your 267 in ``config.ini`` to the list of origins permitted to access your
269 request fails, the api request will be stopped by the browser. 269 request fails, the api request will be stopped by the browser.
270 270
271 The following CORS preflight headers are usually added automatically by 271 The following CORS preflight headers are usually added automatically by
272 the browser and must all be present: 272 the browser and must all be present:
273 273
274 * `Access-Control-Request-Headers` 274 * ``Access-Control-Request-Headers``
275 * `Access-Control-Request-Method` 275 * ``Access-Control-Request-Method``
276 * `Origin` 276 * ``Origin``
277 277
278 The headers of the 204 response depend on the 278 The headers of the 204 response depend on the
279 ``allowed_api_origins`` setting. If a ``*`` is included as the 279 ``allowed_api_origins`` setting. If a ``*`` is included as the
280 first element, any client can read the data but they can not 280 first element, any client can read the data but they can not
281 provide authentication. This limits the available data to what 281 provide authentication. This limits the available data to what
282 the anonymous user can see in the web interface. 282 the anonymous user can see in the web interface.
283 283
284 All 204 responses will include the headers: 284 All 204 responses will include the headers:
285 285
286 * `Access-Control-Allow-Origin` 286 * ``Access-Control-Allow-Origin``
287 * `Access-Control-Allow-Headers` 287 * ``Access-Control-Allow-Headers``
288 * `Access-Control-Allow-Methods` 288 * ``Access-Control-Allow-Methods``
289 * `Access-Control-Max-Age: 86400` 289 * ``Access-Control-Max-Age: 86400``
290 290
291 If the client's ORIGIN header matches an entry besides ``*`` in the 291 If the client's ORIGIN header matches an entry besides ``*`` in the
292 ``allowed_api_origins`` it will also include: 292 ``allowed_api_origins`` it will also include:
293 293
294 * `Access-Control-Allow-Credentials: true` 294 * ``Access-Control-Allow-Credentials: true``
295 295
296 permitting the client to log in and perform authenticated operations. 296 permitting the client to log in and perform authenticated operations.
297 297
298 If the endpoint accepts the PATCH verb the header `Accept-Patch` with 298 If the endpoint accepts the PATCH verb the header ``Accept-Patch`` with
299 valid mime types (usually `application/x-www-form-urlencoded, 299 valid mime types (usually `application/x-www-form-urlencoded,
300 multipart/form-data`) will be included. 300 multipart/form-data`) will be included.
301 301
302 It will also include rate limit headers since the request is included 302 It will also include rate limit headers since the request is included
303 in the rate limit for the URL. The results from the CORS preflight 303 in the rate limit for the URL. The results from the CORS preflight
325 from roundup import rest 325 from roundup import rest
326 from dicttoxml import dicttoxml as dtox # from tracker_root/lib directory 326 from dicttoxml import dicttoxml as dtox # from tracker_root/lib directory
327 327
328 rest.dicttoxml = dtox 328 rest.dicttoxml = dtox
329 329
330 .. _interfaces.py: customizing.html#interfaces-py-hooking-into-the-core-of-roundup 330 .. _interfaces.py: customizing.html#interfaces-py-hooking-into-the-core-of-roundup
331 331
332 The rest interface accepts the http accept header and can include 332 The rest interface accepts the http accept header and can include
333 ``q`` values to specify the preferred mechanism. This is the preferred 333 ``q`` values to specify the preferred mechanism. This is the preferred
334 way to specify alternate acceptable response formats. 334 way to specify alternate acceptable response formats.
335 335
378 "meta data field1": "value", 378 "meta data field1": "value",
379 "meta data field2": "value", 379 "meta data field2": "value",
380 "collection": [ 380 "collection": [
381 { "link": "url to item", 381 { "link": "url to item",
382 "id": "internal identifier for item" }, 382 "id": "internal identifier for item" },
383 { "link": "url to second item", 383 { "link": "url to second item",
384 "id": "id item 2" }, 384 "id": "id item 2" },
385 ... ] 385 ... ]
386 "@links": { 386 "@links": {
387 "relation": [ 387 "relation": [
388 { "rel": "relation/subrelation", 388 { "rel": "relation/subrelation",
393 } 393 }
394 } 394 }
395 } 395 }
396 396
397 available meta data is described in the documentation for the 397 available meta data is described in the documentation for the
398 collections endpoint. 398 collections endpoint.
399 399
400 The ``link`` fields implement `HATEOS`_ by supplying a url for the 400 The ``link`` fields implement `HATEOS`_ by supplying a url for the
401 resource represented by that object. The "link" parameter with the 401 resource represented by that object. The "link" parameter with the
402 value of a url is a special case of the @links parameter. 402 value of a url is a special case of the @links parameter.
403 403
427 "meta data field1": "value", 427 "meta data field1": "value",
428 "type": "type of item, issue, user ..." 428 "type": "type of item, issue, user ..."
429 "link": "link to retrieve item", 429 "link": "link to retrieve item",
430 "attributes": { 430 "attributes": {
431 "title": "title of issue", 431 "title": "title of issue",
432 "nosy": [ 432 "nosy": [
433 { "link": "url for user4", 433 { "link": "url for user4",
434 "id": "4" } 434 "id": "4" }
435 ], 435 ],
436 436
437 ... } 437 ... }
438 } 438 }
480 giving the issue-id, e.g., ``/data/issue/42``. Individual properties of 480 giving the issue-id, e.g., ``/data/issue/42``. Individual properties of
481 an item can be queried by appending the property, e.g., 481 an item can be queried by appending the property, e.g.,
482 ``/data/issue/42/title``. 482 ``/data/issue/42/title``.
483 483
484 484
485 All the links mentioned in the following support the http method ``GET``. 485 All the links mentioned in the following support the http method ``GET``.
486 Results of a ``GET`` request will always return the results as a 486 Results of a ``GET`` request will always return the results as a
487 dictionary with the entry ``data`` referring to the returned data. 487 dictionary with the entry ``data`` referring to the returned data.
488 488
489 Details are in the sections below. 489 Details are in the sections below.
490 490
496 ``@total_size``. A a ``collection`` list follows which contains the id 496 ``@total_size``. A a ``collection`` list follows which contains the id
497 and link to the respective item. For example a get on 497 and link to the respective item. For example a get on
498 https://.../rest/data/issue returns:: 498 https://.../rest/data/issue returns::
499 499
500 { 500 {
501 "data": { 501 "data": {
502 "collection": [ 502 "collection": [
503 { 503 {
504 "id": "1", 504 "id": "1",
505 "link": "https://.../rest/data/issue/1" 505 "link": "https://.../rest/data/issue/1"
506 }, 506 },
507 { 507 {
508 "id": "100", 508 "id": "100",
509 "link": "https://.../rest/data/issue/100" 509 "link": "https://.../rest/data/issue/100"
510 } 510 }
511 ... 511 ...
512 ], 512 ],
513 "@total_size": 171 513 "@total_size": 171
514 } 514 }
515 } 515 }
516 516
517 Collection endpoints support a number of features as seen in the next 517 Collection endpoints support a number of features as seen in the next
518 sections. 518 sections.
519 519
570 Searching for strings (e.g. the issue title, or a keyword name) 570 Searching for strings (e.g. the issue title, or a keyword name)
571 performs a case-insensitive substring search. Searching for 571 performs a case-insensitive substring search. Searching for
572 ``title=Something`` (or in long form title~=Something) will find all 572 ``title=Something`` (or in long form title~=Something) will find all
573 issues with "Something" or "someThing", etc. in the title. 573 issues with "Something" or "someThing", etc. in the title.
574 574
575 Changing the search to ``title:=Something`` (note the `:`) performs an 575 Changing the search to ``title:=Something`` (note the ``:``) performs an
576 exact case-sensitive string match for exactly one word ``Something`` 576 exact case-sensitive string match for exactly one word ``Something``
577 with a capital ``S``. Another example is: 577 with a capital ``S``. Another example is:
578 ``title:=test+that+nosy+actually+works.`` where the + signs are spaces 578 ``title:=test+that+nosy+actually+works.`` where the + signs are spaces
579 in the string. Replacing ``+`` with the `URL encoding`_ for space 579 in the string. Replacing ``+`` with the `URL encoding`_ for space
580 ``%20`` will also work. Note that you must match the spaces when 580 ``%20`` will also work. Note that you must match the spaces when
581 performing exact matches. So `title:=test++that+nosy+actually+works.`` 581 performing exact matches. So ``title:=test++that+nosy+actually+works.``
582 matches the word ``test`` with two spaces bewteen ``test`` and 582 matches the word ``test`` with two spaces between ``test`` and
583 ``that`` in the title. 583 ``that`` in the title.
584 584
585 To make this clear, searching 585 To make this clear, searching
586 ``https://.../rest/data/issue?keyword=Foo`` will not work unless there 586 ``https://.../rest/data/issue?keyword=Foo`` will not work unless there
587 is a keyword with a (case sensitive) name field of ``Foo`` which is 587 is a keyword with a (case sensitive) name field of ``Foo`` which is
639 Pagination 639 Pagination
640 ~~~~~~~~~~ 640 ~~~~~~~~~~
641 641
642 Collection endpoints support pagination. This is controlled by query 642 Collection endpoints support pagination. This is controlled by query
643 parameters ``@page_size`` and ``@page_index`` (Note the use of the 643 parameters ``@page_size`` and ``@page_index`` (Note the use of the
644 leading `@` to make the parameters distinguishable from field names.) 644 leading ``@`` to make the parameters distinguishable from field names.)
645 645
646 .. list-table:: Query Parameters Examples 646 .. list-table:: Query Parameters Examples
647 :header-rows: 1 647 :header-rows: 1
648 :widths: 20 80 648 :widths: 20 80
649 :class: valign-top 649 :class: valign-top
659 659
660 Also when pagination is enabled the returned data include pagination 660 Also when pagination is enabled the returned data include pagination
661 links along side the collection data. This looks like:: 661 links along side the collection data. This looks like::
662 662
663 { "data": 663 { "data":
664 { 664 {
665 "collection": { ... }, 665 "collection": { ... },
666 "@total_size": 222, 666 "@total_size": 222,
667 "@links": { 667 "@links": {
668 "self": [ 668 "self": [
669 { 669 {
670 "uri": 670 "uri":
671 "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5", 671 "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5",
672 "rel": "self" 672 "rel": "self"
673 } 673 }
674 ], 674 ],
675 "next": [ 675 "next": [
676 { 676 {
677 "uri": 677 "uri":
678 "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5", 678 "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5",
679 "rel": "next" 679 "rel": "next"
680 } 680 }
681 ] 681 ]
682 } 682 }
683 } 683 }
684 } 684 }
685 685
686 The ``@links`` parameter is a dictionary indexed by 686 The ``@links`` parameter is a dictionary indexed by
687 relationships. Each relationship is a list of one or more full link 687 relationships. Each relationship is a list of one or more full link
745 ``https://.../rest/data/issue?@verbose=2&@fields=status`` returns:: 745 ``https://.../rest/data/issue?@verbose=2&@fields=status`` returns::
746 746
747 747
748 { 748 {
749 "data": { 749 "data": {
750 "collection": [ 750 "collection": [
751 { 751 {
752 "link": "https://.../rest/data/issue/1", 752 "link": "https://.../rest/data/issue/1",
753 "title": "Welcome to the tracker START HERE", 753 "title": "Welcome to the tracker START HERE",
754 "id": "1", 754 "id": "1",
755 "status": { 755 "status": {
756 "link": "https://.../rest/data/status/1", 756 "link": "https://.../rest/data/status/1",
757 "id": "1", 757 "id": "1",
758 "name": "new" 758 "name": "new"
759 } 759 }
760 }, 760 },
761 ... 761 ...
762 } 762 }
763 763
764 the format of the status field (included because of 764 the format of the status field (included because of
765 ``@fields=status``) includes the label for the status. This is due to 765 ``@fields=status``) includes the label for the status. This is due to
766 inclusion of ``@verbose=2``. Without verbose you would see:: 766 inclusion of ``@verbose=2``. Without verbose you would see::
767 767
768 { 768 {
769 "data": { 769 "data": {
770 "collection": [ 770 "collection": [
771 { 771 {
772 "link": "https://.../rest/data/issue/1", 772 "link": "https://.../rest/data/issue/1",
773 "id": "1", 773 "id": "1",
774 "status": { 774 "status": {
775 "link": "https://.../rest/data/status/1", 775 "link": "https://.../rest/data/status/1",
776 "id": "1" 776 "id": "1"
777 } 777 }
778 }, 778 },
779 ... 779 ...
780 } 780 }
781 781
782 Note that the ``link`` field that is returned doesn't exist in the 782 Note that the ``link`` field that is returned doesn't exist in the
783 database. It is a construct of the rest interface. This means that you 783 database. It is a construct of the rest interface. This means that you
799 { 799 {
800 "data": { 800 "data": {
801 "id": "11", 801 "id": "11",
802 "type": "msg", 802 "type": "msg",
803 "link": "https://.../demo/rest/data/msg/11", 803 "link": "https://.../demo/rest/data/msg/11",
804 "attributes": { 804 "attributes": {
805 "author": { 805 "author": {
806 "id": "5", 806 "id": "5",
807 "link": "https://.../demo/rest/data/user/5" 807 "link": "https://.../demo/rest/data/user/5"
808 }, 808 },
809 "content": { 809 "content": {
856 reprehenderit\neu to quisquam velit, passage, 856 reprehenderit\neu to quisquam velit, passage,
857 was or toil BC quis denouncing quia\nexercise, 857 was or toil BC quis denouncing quia\nexercise,
858 veritatis et used voluptas I elit, a The...", 858 veritatis et used voluptas I elit, a The...",
859 "date": "2017-10-30.00:53:15", 859 "date": "2017-10-30.00:53:15",
860 ... 860 ...
861 861
862 Lines are wrapped for display, content value is one really long 862 Lines are wrapped for display, content value is one really long
863 line. If the data is not utf-8 compatible, you will get a link. 863 line. If the data is not utf-8 compatible, you will get a link.
864 864
865 Retrieving the contents of a file is similar. Performing a 865 Retrieving the contents of a file is similar. Performing a
866 get on ``https://.../demo/rest/data/file/11`` returns:: 866 get on ``https://.../demo/rest/data/file/11`` returns::
895 ``https://.../demo/rest/data/file/11?@verbose=3`` the content field 895 ``https://.../demo/rest/data/file/11?@verbose=3`` the content field
896 above is displayed as (wrapped for display):: 896 above is displayed as (wrapped for display)::
897 897
898 "content": "file11 is not text, retrieve using binary_content 898 "content": "file11 is not text, retrieve using binary_content
899 property. mdsum: bd990c0f8833dd991daf610b81b62316", 899 property. mdsum: bd990c0f8833dd991daf610b81b62316",
900
901 900
902 You can use the `binary_content property`_ described below to 901 You can use the `binary_content property`_ described below to
903 retrieve an encoded copy of the data. 902 retrieve an encoded copy of the data.
904 903
905 Other query params 904 Other query params
1095 1094
1096 An example of returned values:: 1095 An example of returned values::
1097 1096
1098 { 1097 {
1099 "data": { 1098 "data": {
1100 "type": "issue", 1099 "type": "issue",
1101 "@etag": "\"f15e6942f00a41960de45f9413684591\"", 1100 "@etag": "\"f15e6942f00a41960de45f9413684591\"",
1102 "link": "https://.../rest/data/issue/23", 1101 "link": "https://.../rest/data/issue/23",
1103 "attributes": { 1102 "attributes": {
1104 "keyword": [], 1103 "keyword": [],
1105 "messages": [ 1104 "messages": [
1106 { 1105 {
1107 "link": "https://.../rest/data/msg/375", 1106 "link": "https://.../rest/data/msg/375",
1108 "id": "375" 1107 "id": "375"
1109 }, 1108 },
1110 { 1109 {
1111 "link": "https://.../rest/data/msg/376", 1110 "link": "https://.../rest/data/msg/376",
1112 "id": "376" 1111 "id": "376"
1113 }, 1112 },
1114 ... 1113 ...
1115 ], 1114 ],
1116 "files": [], 1115 "files": [],
1117 "status": { 1116 "status": {
1118 "link": "https://.../rest/data/status/2", 1117 "link": "https://.../rest/data/status/2",
1119 "id": "2" 1118 "id": "2"
1120 }, 1119 },
1121 "title": "This is a title title", 1120 "title": "This is a title title",
1122 "superseder": [], 1121 "superseder": [],
1123 "nosy": [ 1122 "nosy": [
1124 { 1123 {
1125 "link": "https://.../rest/data/user/4", 1124 "link": "https://.../rest/data/user/4",
1126 "id": "4" 1125 "id": "4"
1127 }, 1126 },
1128 { 1127 {
1129 "link": "https://.../rest/data/user/5", 1128 "link": "https://.../rest/data/user/5",
1130 "id": "5" 1129 "id": "5"
1131 } 1130 }
1132 ], 1131 ],
1133 "assignedto": null, 1132 "assignedto": null,
1134 }, 1133 },
1135 "id": "23" 1134 "id": "23"
1136 } 1135 }
1137 } 1136 }
1138 1137
1139 Retrieve item using key value 1138 Retrieve item using key value
1140 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1139 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1196 "data": { 1195 "data": {
1197 "id": "12", 1196 "id": "12",
1198 "link": "https://.../demo/rest/data/file/12" 1197 "link": "https://.../demo/rest/data/file/12"
1199 } 1198 }
1200 } 1199 }
1201 1200
1202 1201
1203 Other Supported Methods for Items 1202 Other Supported Methods for Items
1204 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1203 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1205 1204
1206 The method ``PUT`` is allowed on individual items, e.g. 1205 The method ``PUT`` is allowed on individual items, e.g.
1218 --data '{ "nosy": [ "1", "5" ] }' \ 1217 --data '{ "nosy": [ "1", "5" ] }' \
1219 "https://example.com/demo/rest/data/issue/23" 1218 "https://example.com/demo/rest/data/issue/23"
1220 1219
1221 { 1220 {
1222 "data": { 1221 "data": {
1223 "attribute": { 1222 "attribute": {
1224 "nosy": [ 1223 "nosy": [
1225 "1", 1224 "1",
1226 "5" 1225 "5"
1227 ] 1226 ]
1228 }, 1227 },
1229 "type": "issue", 1228 "type": "issue",
1230 "link": "https://example.com/demo/rest/data/issue/23", 1229 "link": "https://example.com/demo/rest/data/issue/23",
1231 "id": "23" 1230 "id": "23"
1232 } 1231 }
1233 } 1232 }
1234 1233
1235 If the above command is repeated with the data attribute:: 1234 If the above command is repeated with the data attribute::
1236 1235
1238 1237
1239 this is returned:: 1238 this is returned::
1240 1239
1241 { 1240 {
1242 "data": { 1241 "data": {
1243 "attribute": { 1242 "attribute": {
1244 "title": "This is now my title" 1243 "title": "This is now my title"
1245 }, 1244 },
1246 "type": "issue", 1245 "type": "issue",
1247 "link": 1246 "link":
1248 "https://.../demo/rest/data/issue/23", 1247 "https://.../demo/rest/data/issue/23",
1249 "id": "23" 1248 "id": "23"
1250 } 1249 }
1251 } 1250 }
1252 1251
1253 Note that nosy is not in the attributes returned. It is the same as 1252 Note that nosy is not in the attributes returned. It is the same as
1254 before, so no change has happened and it is not reported. 1253 before, so no change has happened and it is not reported.
1264 "https://.../demo/rest/data/issue/23" 1263 "https://.../demo/rest/data/issue/23"
1265 1264
1266 which returns both title and nosy attributes:: 1265 which returns both title and nosy attributes::
1267 1266
1268 { 1267 {
1269 "data": { 1268 "data": {
1270 "attribute": { 1269 "attribute": {
1271 "title": "This is now my new title", 1270 "title": "This is now my new title",
1272 "nosy": [ 1271 "nosy": [
1273 "4", 1272 "4",
1274 "5" 1273 "5"
1275 ] 1274 ]
1276 }, 1275 },
1277 "type": "issue", 1276 "type": "issue",
1278 "link": 1277 "link":
1279 "https://.../demo/rest/data/issue/23", 1278 "https://.../demo/rest/data/issue/23",
1280 "id": "23" 1279 "id": "23"
1281 } 1280 }
1282 } 1281 }
1283 1282
1284 Note that mixing url query parameters with payload submission doesn't 1283 Note that mixing url query parameters with payload submission doesn't
1285 work. So using:: 1284 work. So using::
1286 1285
1330 1329
1331 which returns:: 1330 which returns::
1332 1331
1333 { 1332 {
1334 "data": { 1333 "data": {
1335 "attribute": { 1334 "attribute": {
1336 "nosy": [ 1335 "nosy": [
1337 "3", 1336 "3",
1338 "4" 1337 "4"
1339 ] 1338 ]
1340 }, 1339 },
1341 "type": "issue", 1340 "type": "issue",
1342 "link": "https://.../rest/data/issue/23", 1341 "link": "https://.../rest/data/issue/23",
1343 "id": "23" 1342 "id": "23"
1344 } 1343 }
1345 } 1344 }
1346 1345
1347 Note that the changed values are returned so you can update 1346 Note that the changed values are returned so you can update
1348 internal state in your app with the new data. 1347 internal state in your app with the new data.
1364 1363
1365 For example:: 1364 For example::
1366 1365
1367 { 1366 {
1368 "data": { 1367 "data": {
1369 "link": "https://.../rest/data/issue/22/title", 1368 "link": "https://.../rest/data/issue/22/title",
1370 "data": "I need Broken PC", 1369 "data": "I need Broken PC",
1371 "type": "<class 'str'>", 1370 "type": "<class 'str'>",
1372 "id": "22", 1371 "id": "22",
1373 "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\"" 1372 "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\""
1374 } 1373 }
1375 } 1374 }
1376 1375
1377 1376
1378 All endpoints support an ``OPTIONS`` method for determining which 1377 All endpoints support an ``OPTIONS`` method for determining which
1392 "link": "https://.../demo/rest/data/msg/11/content", 1391 "link": "https://.../demo/rest/data/msg/11/content",
1393 "data": "of has to who pleasure. or of account give because the 1392 "data": "of has to who pleasure. or of account give because the
1394 reprehenderit\neu to quisquam velit, passage, was or...", 1393 reprehenderit\neu to quisquam velit, passage, was or...",
1395 "@etag": "\"584f82231079e349031bbb853747df1c\"" 1394 "@etag": "\"584f82231079e349031bbb853747df1c\""
1396 } 1395 }
1397 } 1396 }
1398 1397
1399 (the content property is wrapped for display, it is one long line.) 1398 (the content property is wrapped for display, it is one long line.)
1400 1399
1401 .. _binary_content property: 1400 .. _binary_content property:
1402 1401
1495 1494
1496 1495
1497 >>> import requests 1496 >>> import requests
1498 >>> u = 'http://user:password@tracker.example.com/demo/rest/data/' 1497 >>> u = 'http://user:password@tracker.example.com/demo/rest/data/'
1499 >>> s = requests.session() 1498 >>> s = requests.session()
1500 >>> session.auth = ('admin', 'admin') 1499 >>> session.auth = ('admin', 'admin')
1501 >>> r = s.get(u + 'issue/42/title') 1500 >>> r = s.get(u + 'issue/42/title')
1502 >>> if r.status_code != 200: 1501 >>> if r.status_code != 200:
1503 ... print("Failed: %s: %s" % (r.status_code, r.reason)) 1502 ... print("Failed: %s: %s" % (r.status_code, r.reason))
1504 ... exit(1) 1503 ... exit(1)
1505 >>> print (r.json() ['data']['data'] 1504 >>> print (r.json() ['data']['data']
1517 >>> r = s.get (u + 'issue/42') 1516 >>> r = s.get (u + 'issue/42')
1518 >>> etag = r.headers['ETag'] 1517 >>> etag = r.headers['ETag']
1519 >>> print("ETag: %s" % etag) 1518 >>> print("ETag: %s" % etag)
1520 >>> etag = r.json()['data']['@etag'] 1519 >>> etag = r.json()['data']['@etag']
1521 >>> print("@etag: %s" % etag) 1520 >>> print("@etag: %s" % etag)
1522 >>> h = {'If-Match': etag, 1521 >>> h = {'If-Match': etag,
1523 ... 'X-Requested-With': 'rest', 1522 ... 'X-Requested-With': 'rest',
1524 ... 'Referer': 'http://tracker.example.com/demo/'} 1523 ... 'Referer': 'http://tracker.example.com/demo/'}
1525 >>> d = {'@op:'action', '@action_name':'retire'} 1524 >>> d = {'@op:'action', '@action_name':'retire'}
1526 >>> r = s.patch(u + 'issue/42', data = d, headers = h) 1525 >>> r = s.patch(u + 'issue/42', data = d, headers = h)
1527 >>> print(r.json()) 1526 >>> print(r.json())
1539 1538
1540 A similar curl based retire example is to use:: 1539 A similar curl based retire example is to use::
1541 1540
1542 curl -s -u admin:admin \ 1541 curl -s -u admin:admin \
1543 -H "Referer: https://tracker.example.com/demo/" \ 1542 -H "Referer: https://tracker.example.com/demo/" \
1544 -H "X-requested-with: rest" \ 1543 -H "X-requested-with: rest" \
1545 -H "Content-Type: application/json" \ 1544 -H "Content-Type: application/json" \
1546 https://tracker.example.com/demo/rest/data/status/1 1545 https://tracker.example.com/demo/rest/data/status/1
1547 1546
1548 to get the etag manually. Then insert the etag in the If-Match header 1547 to get the etag manually. Then insert the etag in the If-Match header
1549 for this retire example:: 1548 for this retire example::
1550 1549
1551 curl -s -u admin:admin \ 1550 curl -s -u admin:admin \
1552 -H "Referer: https://tracker.example.com/demo/" \ 1551 -H "Referer: https://tracker.example.com/demo/" \
1553 -H "X-requested-with: rest" \ 1552 -H "X-requested-with: rest" \
1554 -H "Content-Type: application/json" \ 1553 -H "Content-Type: application/json" \
1555 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \ 1554 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
1556 --data-raw '{ "@op":"action", "@action_name": "retire" }'\ 1555 --data-raw '{ "@op":"action", "@action_name": "retire" }'\
1557 -X PATCH \ 1556 -X PATCH \
1558 https://tracker.example.com/demo/rest/data/status/1 1557 https://tracker.example.com/demo/rest/data/status/1
1559 1558
1560 and restore:: 1559 and restore::
1561 1560
1562 curl -s -u admin:admin \ 1561 curl -s -u admin:admin \
1563 -H "Referer: https://tracker.example.com/demo/" \ 1562 -H "Referer: https://tracker.example.com/demo/" \
1564 -H "X-requested-with: rest" \ 1563 -H "X-requested-with: rest" \
1565 -H "Content-Type: application/json" \ 1564 -H "Content-Type: application/json" \
1566 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \ 1565 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
1567 --data-raw '{ "@op":"action", "@action_name": "restore" }'\ 1566 --data-raw '{ "@op":"action", "@action_name": "restore" }'\
1568 -X PATCH \ 1567 -X PATCH \
1569 https://tracker.example.com/demo/rest/data/status/1 1568 https://tracker.example.com/demo/rest/data/status/1
1570 1569
1571 1570
1572 Searches and selection 1571 Searches and selection
1573 ---------------------- 1572 ----------------------
1574 1573
1583 1582
1584 Consider a multi-select box for the superseder property. Using 1583 Consider a multi-select box for the superseder property. Using
1585 selectize.js (and jquery) code similar to:: 1584 selectize.js (and jquery) code similar to::
1586 1585
1587 $('#superseder').selectize({ 1586 $('#superseder').selectize({
1588 valueField: 'id', 1587 valueField: 'id',
1589 labelField: 'title', 1588 labelField: 'title',
1590 searchField: 'title', ... 1589 searchField: 'title', ...
1591 load: function(query, callback) { 1590 load: function(query, callback) {
1592 if (!query.length) return callback(); 1591 if (!query.length) return callback();
1593 $.ajax({ 1592 $.ajax({
1594 url: '.../rest/data/issue?@verbose=2&title=' 1593 url: '.../rest/data/issue?@verbose=2&title='
1595 + encodeURIComponent(query), 1594 + encodeURIComponent(query),
1596 type: 'GET', 1595 type: 'GET',
1597 error: function() {callback();}, 1596 error: function() {callback();},
1598 success: function(res) { 1597 success: function(res) {
1599 callback(res.data.collection);} 1598 callback(res.data.collection);}
1600 1599
1601 Sets up a box that a user can type the word "request" into. Then 1600 Sets up a box that a user can type the word "request" into. Then
1602 selectize.js will use that word to generate an ajax request with the 1601 selectize.js will use that word to generate an ajax request with the
1603 url: ``.../rest/data/issue?@verbose=2&title=request`` 1602 url: ``.../rest/data/issue?@verbose=2&title=request``
1604 1603
1607 { 1606 {
1608 "data": { 1607 "data": {
1609 "@total_size": 440, 1608 "@total_size": 440,
1610 "collection": [ 1609 "collection": [
1611 { 1610 {
1612 "link": ".../rest/data/issue/8", 1611 "link": ".../rest/data/issue/8",
1613 "id": "8", 1612 "id": "8",
1614 "title": "Request for Power plugs" 1613 "title": "Request for Power plugs"
1615 }, 1614 },
1616 { 1615 {
1617 "link": ".../rest/data/issue/27", 1616 "link": ".../rest/data/issue/27",
1618 "id": "27", 1617 "id": "27",
1619 "title": "Request for foo" 1618 "title": "Request for foo"
1620 }, 1619 },
1621 ... 1620 ...
1622 1621
1623 selectize.js will look at these objects (as passed to 1622 selectize.js will look at these objects (as passed to
1624 callback(res.data.collection)) and create a select list from the each 1623 callback(res.data.collection)) and create a select list from the each
1682 from roundup.rest import Routing, RestfulInstance, _data_decorator 1681 from roundup.rest import Routing, RestfulInstance, _data_decorator
1683 from roundup.exceptions import Unauthorised 1682 from roundup.exceptions import Unauthorised
1684 1683
1685 class RestfulInstance: 1684 class RestfulInstance:
1686 1685
1687 @Routing.route("/summary2") 1686 @Routing.route("/summary2")
1688 @_data_decorator 1687 @_data_decorator
1689 def summary2(self, input): 1688 def summary2(self, input):
1690 result = { "hello": "world" } 1689 result = { "hello": "world" }
1691 return 200, result 1690 return 200, result
1692 1691
1693 will make a new endpoint .../rest/summary2 that you can test with:: 1692 will make a new endpoint .../rest/summary2 that you can test with::
1694 1693
1695 $ curl -X GET .../rest/summary2 1694 $ curl -X GET .../rest/summary2
1696 { 1695 {
1702 Similarly appending this to interfaces.py after summary2:: 1701 Similarly appending this to interfaces.py after summary2::
1703 1702
1704 # handle more endpoints 1703 # handle more endpoints
1705 @Routing.route("/data/<:class_name>/@schema", 'GET') 1704 @Routing.route("/data/<:class_name>/@schema", 'GET')
1706 def get_element_schema(self, class_name, input): 1705 def get_element_schema(self, class_name, input):
1707 result = { "schema": {} } 1706 result = { "schema": {} }
1708 uid = self.db.getuid () 1707 uid = self.db.getuid ()
1709 if not self.db.security.hasPermission('View', uid, class_name) : 1708 if not self.db.security.hasPermission('View', uid, class_name) :
1710 raise Unauthorised('Permission to view %s denied' % class_name) 1709 raise Unauthorised('Permission to view %s denied' % class_name)
1711 1710
1712 class_obj = self.db.getclass(class_name) 1711 class_obj = self.db.getclass(class_name)
1713 props = class_obj.getprops(protected=False) 1712 props = class_obj.getprops(protected=False)
1714 schema = result['schema'] 1713 schema = result['schema']
1715 1714
1716 for prop in props: 1715 for prop in props:
1717 schema[prop] = { "type": repr(class_obj.properties[prop]) } 1716 schema[prop] = { "type": repr(class_obj.properties[prop]) }
1718 1717
1719 return result 1718 return result
1720 1719
1721 .. 1720 ..
1722 the # comment in the example is needed to preserve indention under Class. 1721 the # comment in the example is needed to preserve indention under Class.
1723 1722
1724 returns some data about the class:: 1723 returns some data about the class::
1725 1724
1726 $ curl -X GET .../rest/data/issue/@schema 1725 $ curl -X GET .../rest/data/issue/@schema
1727 { 1726 {
1728 "schema": { 1727 "schema": {
1729 "keyword": { 1728 "keyword": {
1730 "type": "<roundup.hyperdb.Multilink to \"keyword\">" 1729 "type": "<roundup.hyperdb.Multilink to \"keyword\">"
1731 }, 1730 },
1732 "title": { 1731 "title": {
1733 "type": "<roundup.hyperdb.String>" 1732 "type": "<roundup.hyperdb.String>"
1734 }, 1733 },
1735 "files": { 1734 "files": {
1736 "type": "<roundup.hyperdb.Multilink to \"file\">" 1735 "type": "<roundup.hyperdb.Multilink to \"file\">"
1737 }, 1736 },
1738 "status": { 1737 "status": {
1739 "type": "<roundup.hyperdb.Link to \"status\">" 1738 "type": "<roundup.hyperdb.Link to \"status\">"
1740 }, ... 1739 }, ...
1741 } 1740 }
1742 } 1741 }
1743 1742
1744 1743
1745 Adding other endpoints (e.g. to allow an OPTIONS query against 1744 Adding other endpoints (e.g. to allow an OPTIONS query against
1746 ``/data/issue/@schema``) is left as an exercise for the reader. 1745 ``/data/issue/@schema``) is left as an exercise for the reader.
1797 However the templating system can access the hyperdb directly which 1796 However the templating system can access the hyperdb directly which
1798 allows filtering to happen with admin privs escaping the standard 1797 allows filtering to happen with admin privs escaping the standard
1799 permissions scheme. For example access to a user's roles should be 1798 permissions scheme. For example access to a user's roles should be
1800 limited to the user (read only) and an admin. If you have customised 1799 limited to the user (read only) and an admin. If you have customised
1801 your schema to implement `Restricting the list of 1800 your schema to implement `Restricting the list of
1802 users that are assignable to a task <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__ 1801 users that are assignable to a task
1802 <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__
1803 so that only users with a 1803 so that only users with a
1804 Developer role are allowed to be assigned to an issue, a rest end 1804 Developer role are allowed to be assigned to an issue, a rest end
1805 point must be added to provide a view that exposes users with this 1805 point must be added to provide a view that exposes users with this
1806 permission. 1806 permission.
1807 1807
1808 Using the normal ``/data/user?roles=Developer`` will return all the 1808 Using the normal ``/data/user?roles=Developer`` will return all the
1809 users in the system unless you are an admin user because most users 1809 users in the system unless you are an admin user because most users
1810 can't see the roles. Building on the `Adding new rest endpoints`_ 1810 can't see the roles. Building on the `Adding new rest endpoints`_
1811 section this code adds a new endpoint `/data/@permission/Developer` 1811 section this code adds a new endpoint ``/data/@permission/Developer``
1812 that returns a list of users with the developer role:: 1812 that returns a list of users with the developer role::
1813 1813
1814 from roundup.rest import Routing, RestfulInstance 1814 from roundup.rest import Routing, RestfulInstance
1815 from roundup.anypy.cgi_ import MiniFieldStorage 1815 from roundup.anypy.cgi_ import MiniFieldStorage
1816 1816
1817 class RestfulInstance(object): 1817 class RestfulInstance(object):
1818 1818
1819 @Routing.route("/data/@permission/Developer") 1819 @Routing.route("/data/@permission/Developer")
1820 def get_role_Developer(self, input): 1820 def get_role_Developer(self, input):
1821 '''An endpoint to return a list of users with Developer 1821 '''An endpoint to return a list of users with Developer
1822 role who can be assigned to an issue. 1822 role who can be assigned to an issue.
1823 1823
1824 It ignores attempt to search by any property except 1824 It ignores attempt to search by any property except
1825 username and realname. It also ignores the whole @fields 1825 username and realname. It also ignores the whole @fields
1826 specification if it specifies a property the user 1826 specification if it specifies a property the user
1827 can't view. Other @ query params (e.g. @page... and 1827 can't view. Other @ query params (e.g. @page... and
1828 @verbose) are supported. 1828 @verbose) are supported.
1829 1829
1830 It assumes admin access rights so that the roles property 1830 It assumes admin access rights so that the roles property
1831 of the user can be searched. This is needed if the roles 1831 of the user can be searched. This is needed if the roles
1832 property is not searchable/viewable by normal users. A user 1832 property is not searchable/viewable by normal users. A user
1833 who can search roles can identify users with the admin 1833 who can search roles can identify users with the admin
1834 role. So it does not respond the same as a rest/data/users 1834 role. So it does not respond the same as a rest/data/users
1835 search by a non-admin user. 1835 search by a non-admin user.
1836 ''' 1836 '''
1837 # get real user id 1837 # get real user id
1838 realuid=self.db.getuid() 1838 realuid=self.db.getuid()
1839 1839
1840 def allowed_field(fs): 1840 def allowed_field(fs):
1841 if fs.name in ['username', 'realname' ]: 1841 if fs.name in ['username', 'realname' ]:
1842 # only allow search matches for these fields 1842 # only allow search matches for these fields
1843 return True 1843 return True
1844 elif fs.name in [ '@fields' ]: 1844 elif fs.name in [ '@fields' ]:
1845 for prop in fs.value.split(','): 1845 for prop in fs.value.split(','):
1846 # if any property is unviewable to user, remove 1846 # if any property is unviewable to user, remove
1847 # @field entry. If they can't see it for the admin 1847 # @field entry. If they can't see it for the admin
1848 # user, don't let them see it for any user. 1848 # user, don't let them see it for any user.
1849 if not self.db.security.hasPermission( 1849 if not self.db.security.hasPermission(
1850 'View', realuid, 'user', property=prop, 1850 'View', realuid, 'user', property=prop,
1851 itemid='1'): 1851 itemid='1'):
1852 return False 1852 return False
1853 return True 1853 return True
1854 elif fs.name.startswith("@"): 1854 elif fs.name.startswith("@"):
1855 # allow @page..., @verbose etc. 1855 # allow @page..., @verbose etc.
1856 return True 1856 return True
1857 1857
1858 # deny all other url parmeters 1858 # deny all other url parmeters
1859 return False 1859 return False
1860 1860
1861 # Cleanup input.list to prevent user from probing roles 1861 # Cleanup input.list to prevent user from probing roles
1862 # or viewing things the user should not be able to view. 1862 # or viewing things the user should not be able to view.
1863 input.list[:] = [ fs for fs in input.list 1863 input.list[:] = [ fs for fs in input.list
1864 if allowed_field(fs) ] 1864 if allowed_field(fs) ]
1865 1865
1866 # Add the role filter required to implement the permission 1866 # Add the role filter required to implement the permission
1867 # search 1867 # search
1868 input.list.append(MiniFieldStorage("roles", "Developer")) 1868 input.list.append(MiniFieldStorage("roles", "Developer"))
1869 1869
1870 # change user to acquire permission to search roles 1870 # change user to acquire permission to search roles
1871 self.db.setCurrentUser('admin') 1871 self.db.setCurrentUser('admin')
1872 1872
1873 # Once we have cleaned up the request, pass it to 1873 # Once we have cleaned up the request, pass it to
1874 # get_collection as though /rest/data/users?... has been called 1874 # get_collection as though /rest/data/users?... has been called
1875 # to get @verbose and other args supported. 1875 # to get @verbose and other args supported.
1876 return self.get_collection('user', input) 1876 return self.get_collection('user', input)
1877 1877
1878 Calling this with:: 1878 Calling this with::
1879 1879
1880 curl 'http://example.com/demo/rest/data/@permission/Developer?@fields=realname&roles=Users&@verbose=2' 1880 curl 'http://example.com/demo/rest/data/@permission/Developer?@fields=realname&roles=Users&@verbose=2'
1881 1881
1882 produces output similar to:: 1882 produces output similar to::
1883 1883
1884 { 1884 {
1885 "data": { 1885 "data": {
1886 "collection": [ 1886 "collection": [
1887 { 1887 {
1888 "username": "agent", 1888 "username": "agent",
1889 "link": http://example.com/demo/rest/data/user/4", 1889 "link": http://example.com/demo/rest/data/user/4",
1890 "realname": "James Bond", 1890 "realname": "James Bond",
1891 "id": "4" 1891 "id": "4"
1892 } 1892 }
1893 ], 1893 ],
1894 "@total_size": 1 1894 "@total_size": 1
1895 } 1895 }
1896 } 1896 }
1897 1897
1898 assuming user 4 is the only user with the Developer role. Note that 1898 assuming user 4 is the only user with the Developer role. Note that
1899 the url passes the ``roles=User`` filter option which is silently 1899 the url passes the ``roles=User`` filter option which is silently
1900 ignored. 1900 ignored.
1938 timelog links to the ``times`` property of the issue. 1938 timelog links to the ``times`` property of the issue.
1939 1939
1940 Create role 1940 Create role
1941 ~~~~~~~~~~~ 1941 ~~~~~~~~~~~
1942 1942
1943 Adding this snippet of code to the tracker's ``schema.py`` should create a role with the 1943 Adding this snippet of code to the tracker's ``schema.py`` should
1944 proper authorization:: 1944 create a role with the proper authorization::
1945 1945
1946 db.security.addRole(name="User:timelog", 1946 db.security.addRole(name="User:timelog",
1947 description="allow a user to create and append timelogs") 1947 description="allow a user to create and append timelogs")
1948 1948
1949 db.security.addPermissionToRole('User:timelog', 'Rest Access') 1949 db.security.addPermissionToRole('User:timelog', 'Rest Access')
2072 else: 2072 else:
2073 claim['roles'] = user_roles 2073 claim['roles'] = user_roles
2074 secret = self.db.config.WEB_JWT_SECRET 2074 secret = self.db.config.WEB_JWT_SECRET
2075 myjwt = jwt.encode(claim, secret, algorithm='HS256') 2075 myjwt = jwt.encode(claim, secret, algorithm='HS256')
2076 2076
2077 # if jwt.__version__ >= 2.0.0 jwt.encode() returns string 2077 # if jwt.__version__ >= 2.0.0 jwt.encode() returns string
2078 # not byte. So do not use b2s() with newer versions of pyjwt. 2078 # not byte. So do not use b2s() with newer versions of pyjwt.
2079 result = {"jwt": b2s(myjwt), 2079 result = {"jwt": b2s(myjwt),
2080 } 2080 }
2081 2081
2082 return 200, result 2082 return 200, result
2083 2083
2141 curl -s -H "Referer: https://.../demo/" \ 2141 curl -s -H "Referer: https://.../demo/" \
2142 -H "X-requested-with: rest" \ 2142 -H "X-requested-with: rest" \
2143 https://.../demo/rest/JWT/validate?JWT=eyJ0eXAiOiJK...XxMDb-Q3oCnMpyhxPXMAk 2143 https://.../demo/rest/JWT/validate?JWT=eyJ0eXAiOiJK...XxMDb-Q3oCnMpyhxPXMAk
2144 2144
2145 (note no login is required) which returns:: 2145 (note no login is required) which returns::
2146 2146
2147 { 2147 {
2148 "data": { 2148 "data": {
2149 "user": "3", 2149 "user": "3",
2150 "roles": [ 2150 "roles": [
2151 "user:timelog" 2151 "user:timelog"
2153 "iss": "https://.../demo/", 2153 "iss": "https://.../demo/",
2154 "aud": "https://.../demo/", 2154 "aud": "https://.../demo/",
2155 "iat": 1569542404, 2155 "iat": 1569542404,
2156 "exp": 1569546004 2156 "exp": 1569546004
2157 } 2157 }
2158 } 2158 }
2159 2159
2160 2160
2161 There is an issue for `thoughts on JWT credentials`_ that you can view 2161 There is an issue for `thoughts on JWT credentials`_ that you can view
2162 for ideas or add your own. 2162 for ideas or add your own.
2163 2163
2210 2210
2211 uid = self.db.getuid() 2211 uid = self.db.getuid()
2212 class_obj = self.db.getclass('user') 2212 class_obj = self.db.getclass('user')
2213 node = class_obj.getnode(uid) 2213 node = class_obj.getnode(uid)
2214 2214
2215 # set value to 0 to use WEB_API_CALLS_PER_INTERVAL 2215 # set value to 0 to use WEB_API_CALLS_PER_INTERVAL
2216 user_calls = node.__getattr__('rate_limit_calls') 2216 user_calls = node.__getattr__('rate_limit_calls')
2217 # set to 0 to use WEB_API_INTERVAL_IN_SEC 2217 # set to 0 to use WEB_API_INTERVAL_IN_SEC
2218 user_interval = node.__getattr__('rate_limit_interval') 2218 user_interval = node.__getattr__('rate_limit_interval')
2219 2219
2220 return RateLimit(user_calls or calls, 2220 return RateLimit(user_calls or calls,
2221 timedelta(seconds=(user_interval or interval))) 2221 timedelta(seconds=(user_interval or interval)))
2222 else: 2222 else:
2223 # disable rate limiting if either parameter is 0 2223 # disable rate limiting if either parameter is 0
2224 return None 2224 return None
2225 2225
2226 RestfulInstance.getRateLimit = grl 2226 RestfulInstance.getRateLimit = grl

Roundup Issue Tracker: http://roundup-tracker.org/