Mercurial > p > roundup > code
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 |
