comparison doc/rest.txt @ 8416:370689471a08 issue2550923_computed_property

merge from default branch accumulated changes since Nov 2023
author John Rouillard <rouilj@ieee.org>
date Sun, 17 Aug 2025 16:12:25 -0400
parents e7dc47f4d501
children 1ffa1f42e1da
comparison
equal deleted inserted replaced
7693:78585199552a 8416:370689471a08
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
16 :depth: 3 16 :depth: 3
17 17
18 Introduction 18 Introduction
19 ============ 19 ============
20 20
21 After the last 1.6.0 Release, a REST-API developed in 2015 during a 21 After the 1.6.0 Release, a REST-API developed in 2015 during a
22 Google Summer of Code (GSOC) by Chau Nguyen, supervised by Ezio 22 Google Summer of Code (GSOC) by Chau Nguyen, supervised by Ezio
23 Melotti was integrated. The code was updated by Ralf Schlatterbeck and 23 Melotti was integrated. The code was updated by Ralf Schlatterbeck and
24 John Rouillard to address some limitations and incorporate essential 24 John Rouillard to address some limitations and incorporate essential
25 features for a single page web application, such as etag support, 25 features for a single page web application, such as etag support,
26 pagination, and field embedding, among others. 26 pagination, and field embedding, among others.
66 .. _upgrading directions: upgrading.html 66 .. _upgrading directions: upgrading.html
67 67
68 Preventing CSRF Attacks 68 Preventing CSRF Attacks
69 ----------------------- 69 -----------------------
70 70
71 Clients should set the header X-REQUESTED-WITH to any value and the 71 Clients should set the header ``X-REQUESTED-WITH`` to any value and the
72 tracker's config.ini should have ``csrf_enforce_header_x-requested-with 72 tracker's config.ini should have ``csrf_enforce_header_x-requested-with
73 = yes`` or ``required``. 73 = yes`` or ``required``.
74 74
75 If you want to allow Roundup's api to be accessed by an application 75 If you want to allow Roundup's api to be accessed by an application
76 that is not hosted at the same origin as Roundup, you must permit 76 that is not hosted at the same origin as Roundup, you must permit
77 the origin using the ``allowed_api_origins`` setting in 77 the origin using the ``allowed_api_origins`` setting in
78 ``config.ini``. 78 ``config.ini``.
79
80 If you access the REST interface with a method other than ``GET``, you
81 must also supply an origin header with a value that is either the
82 default origin (the URL of the tracker without the path component set in
83 the config file as ``web`` in section ``[tracker]``) or one that is
84 permitted by ``allowed_api_origins``.
79 85
80 Rate Limiting API Failed Logins 86 Rate Limiting API Failed Logins
81 ------------------------------- 87 -------------------------------
82 88
83 To make brute force password guessing harder, the REST API has an 89 To make brute force password guessing harder, the REST API has an
149 meaning that under heavy load, it may miscount and allow more than the 155 meaning that under heavy load, it may miscount and allow more than the
150 burst count. On slower hardware, errors of up to 10% have been 156 burst count. On slower hardware, errors of up to 10% have been
151 observed. Using redis, PostgreSQL, or MySQL for storing ephemeral data 157 observed. Using redis, PostgreSQL, or MySQL for storing ephemeral data
152 minimizes the loss. 158 minimizes the loss.
153 159
160 Limit Size of Returned Data
161 ---------------------------
162
163 When selecting from the database, you can limit the number of rows
164 returned by adding the following to `interfaces.py`_::
165
166 from roundup.rest import RestfulInstance
167 RestfulInstance.max_response_row_size = 26
168
169 This will limit the setting of ``@page_size`` to 25 (one less than the
170 value). If the url includes a ``@page_size`` pagination value greater
171 than or equal to the ``max_response_row_size`` you will receive an
172 error like::
173
174 {
175 "error": {
176 "status": 400,
177 "msg": "Page size 30 must be less than admin limit on query
178 result size: 26."
179 }
180 }
181
182 The default value is 10 million and one rows.
183
154 Client API 184 Client API
155 ========== 185 ==========
156 186
157 The top-level REST url ``/rest/`` will display the current version of 187 The top-level REST url ``/rest/`` will display the current version of
158 the REST API (Version 1 as of this writing) and some links to relevant 188 the REST API (Version 1 as of this writing) and some links to relevant
257 287
258 CORS preflight requests are done using the OPTIONS method. They 288 CORS preflight requests are done using the OPTIONS method. They
259 require that REST be enabled. These requests do not make any changes 289 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 290 or get any information from the database. As a result they are
261 available to the anonymous user and any authenticated user. The user 291 available to the anonymous user and any authenticated user. The user
262 does not need to have `Rest Access` permissions. Also these requests 292 does not need to have ``Rest Access`` permissions. Also these requests
263 bypass CSRF checks except for the Origin header check which is always 293 bypass CSRF checks except for the Origin header check which is always
264 run for preflight requests. 294 run for preflight requests.
265 295
266 You can permit only allowed ORIGINS by setting ``allowed_api_origins`` 296 You can permit only allowed ORIGINS by setting ``allowed_api_origins``
267 in ``config.ini`` to the list of origins permitted to access your 297 in ``config.ini`` to the list of origins permitted to access your
269 request fails, the api request will be stopped by the browser. 299 request fails, the api request will be stopped by the browser.
270 300
271 The following CORS preflight headers are usually added automatically by 301 The following CORS preflight headers are usually added automatically by
272 the browser and must all be present: 302 the browser and must all be present:
273 303
274 * `Access-Control-Request-Headers` 304 * ``Access-Control-Request-Headers``
275 * `Access-Control-Request-Method` 305 * ``Access-Control-Request-Method``
276 * `Origin` 306 * ``Origin``
277 307
278 The headers of the 204 response depend on the 308 The headers of the 204 response depend on the
279 ``allowed_api_origins`` setting. If a ``*`` is included as the 309 ``allowed_api_origins`` setting. If a ``*`` is included as the
280 first element, any client can read the data but they can not 310 first element, any client can read the data but they can not
281 provide authentication. This limits the available data to what 311 provide authentication. This limits the available data to what
282 the anonymous user can see in the web interface. 312 the anonymous user can see in the web interface.
283 313
284 All 204 responses will include the headers: 314 All 204 responses will include the headers:
285 315
286 * `Access-Control-Allow-Origin` 316 * ``Access-Control-Allow-Origin``
287 * `Access-Control-Allow-Headers` 317 * ``Access-Control-Allow-Headers``
288 * `Access-Control-Allow-Methods` 318 * ``Access-Control-Allow-Methods``
289 * `Access-Control-Max-Age: 86400` 319 * ``Access-Control-Max-Age: 86400``
290 320
291 If the client's ORIGIN header matches an entry besides ``*`` in the 321 If the client's ORIGIN header matches an entry besides ``*`` in the
292 ``allowed_api_origins`` it will also include: 322 ``allowed_api_origins`` it will also include:
293 323
294 * `Access-Control-Allow-Credentials: true` 324 * ``Access-Control-Allow-Credentials: true``
295 325
296 permitting the client to log in and perform authenticated operations. 326 permitting the client to log in and perform authenticated operations.
297 327
298 If the endpoint accepts the PATCH verb the header `Accept-Patch` with 328 If the endpoint accepts the PATCH verb the header ``Accept-Patch`` with
299 valid mime types (usually `application/x-www-form-urlencoded, 329 valid mime types (usually `application/x-www-form-urlencoded,
300 multipart/form-data`) will be included. 330 multipart/form-data`) will be included.
301 331
302 It will also include rate limit headers since the request is included 332 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 333 in the rate limit for the URL. The results from the CORS preflight
325 from roundup import rest 355 from roundup import rest
326 from dicttoxml import dicttoxml as dtox # from tracker_root/lib directory 356 from dicttoxml import dicttoxml as dtox # from tracker_root/lib directory
327 357
328 rest.dicttoxml = dtox 358 rest.dicttoxml = dtox
329 359
330 .. _interfaces.py: customizing.html#interfaces-py-hooking-into-the-core-of-roundup 360 .. _interfaces.py: reference.html#interfaces-py-hooking-into-the-core-of-roundup
331 361
332 The rest interface accepts the http accept header and can include 362 The rest interface accepts the http accept header and can include
333 ``q`` values to specify the preferred mechanism. This is the preferred 363 ``q`` values to specify the preferred mechanism. This is the preferred
334 way to specify alternate acceptable response formats. 364 way to specify alternate acceptable response formats.
335 365
336 To make testing from the browser easier, you can also append the 366 To make testing from the browser easier, you can also append the
337 extension ``.json`` or ``.xml`` to the path component of the url. This 367 extension ``.json`` or ``.xml`` to the path component of the url. This
338 will force json or xml (if supported) output. If you use an extension 368 will force json or xml (if supported) output. If you use an extension
339 it takes priority over any accept headers. Note the extension does not 369 it takes priority over any accept headers. Note the extension does not
340 work for the ``/rest`` or ``/rest/data`` paths. In these cases it 370 work for the ``/rest`` or ``/rest/data`` paths. In these cases it
341 returs a 404 error. Adding the header ``Accept: application/xml`` 371 returns a 404 error. Adding the header ``Accept: application/xml``
342 allows these paths to return xml data. 372 allows these paths to return xml data.
343 373
344 The rest interface returns status 406 if you use an unrecognized 374 The rest interface returns status 406 if you use an unrecognized
345 extension. You will also get a 406 status if none of the entries in 375 extension. You will also get a 406 status if none of the entries in
346 the accept header are available or if the accept header is invalid. 376 the accept header are available or if the accept header is invalid.
347 377
348 Note: ``dicttoxml2.py`` is an updated version of ``dicttoxml.py``. If 378 Note: ``dicttoxml2.py`` is an updated version of ``dicttoxml.py`` and
349 you are still using Python 2.7 or 3.6, you can use ``dicttoxml.py``. 379 should be used for Roundup running on Python 3.7 or newer.
350 380
381 Also the ``/binary_content`` attribute endpoint can be used to
382 retrieve raw file data in many formats.
351 383
352 General Guidelines 384 General Guidelines
353 ------------------ 385 ------------------
354 386
355 Performing a ``GET`` on an item or property of an item will return an 387 Performing a ``GET`` on an item or property of an item will return an
378 "meta data field1": "value", 410 "meta data field1": "value",
379 "meta data field2": "value", 411 "meta data field2": "value",
380 "collection": [ 412 "collection": [
381 { "link": "url to item", 413 { "link": "url to item",
382 "id": "internal identifier for item" }, 414 "id": "internal identifier for item" },
383 { "link": "url to second item", 415 { "link": "url to second item",
384 "id": "id item 2" }, 416 "id": "id item 2" },
385 ... ] 417 ... ]
386 "@links": { 418 "@links": {
387 "relation": [ 419 "relation": [
388 { "rel": "relation/subrelation", 420 { "rel": "relation/subrelation",
393 } 425 }
394 } 426 }
395 } 427 }
396 428
397 available meta data is described in the documentation for the 429 available meta data is described in the documentation for the
398 collections endpoint. 430 collections endpoint.
399 431
400 The ``link`` fields implement `HATEOS`_ by supplying a url for the 432 The ``link`` fields implement `HATEOS`_ by supplying a url for the
401 resource represented by that object. The "link" parameter with the 433 resource represented by that object. The "link" parameter with the
402 value of a url is a special case of the @links parameter. 434 value of a url is a special case of the @links parameter.
403 435
427 "meta data field1": "value", 459 "meta data field1": "value",
428 "type": "type of item, issue, user ..." 460 "type": "type of item, issue, user ..."
429 "link": "link to retrieve item", 461 "link": "link to retrieve item",
430 "attributes": { 462 "attributes": {
431 "title": "title of issue", 463 "title": "title of issue",
432 "nosy": [ 464 "nosy": [
433 { "link": "url for user4", 465 { "link": "url for user4",
434 "id": "4" } 466 "id": "4" }
435 ], 467 ],
436 468
437 ... } 469 ... }
438 } 470 }
480 giving the issue-id, e.g., ``/data/issue/42``. Individual properties of 512 giving the issue-id, e.g., ``/data/issue/42``. Individual properties of
481 an item can be queried by appending the property, e.g., 513 an item can be queried by appending the property, e.g.,
482 ``/data/issue/42/title``. 514 ``/data/issue/42/title``.
483 515
484 516
485 All the links mentioned in the following support the http method ``GET``. 517 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 518 Results of a ``GET`` request will always return the results as a
487 dictionary with the entry ``data`` referring to the returned data. 519 dictionary with the entry ``data`` referring to the returned data.
488 520
489 Details are in the sections below. 521 Details are in the sections below.
490 522
491 /data/\ *class* Collection 523 /data/\ *class* Collection
492 -------------------------- 524 --------------------------
493 525
494 When performing the ``GET`` method on a class (e.g. ``/data/issue``), 526 When you use the ``GET`` method on a class (like ``/data/issue``), the
495 the ``data`` object includes the number of items available in 527 ``data`` will include the number of available items in
496 ``@total_size``. A a ``collection`` list follows which contains the id 528 ``@total_size``. If the size exceeds the administrative limit (which
497 and link to the respective item. For example a get on 529 is 10 million by default), ``@total_size`` will be set to ``-1``. To
498 https://.../rest/data/issue returns:: 530 navigate to the last page of results, you can use the ``next`` links
531 or increment ``@page_index`` until the result does not include a
532 ``next`` ``@link`` or ``@total_size`` is not ``-1``. The value of the
533 HTTP header ``X-Count-Total`` is the same as ``@total_size``.
534
535 A ``collection`` list contains the id and link to the
536 respective item. For example a get on https://.../rest/data/issue
537 returns::
499 538
500 { 539 {
501 "data": { 540 "data": {
502 "collection": [ 541 "collection": [
503 { 542 {
504 "id": "1", 543 "id": "1",
505 "link": "https://.../rest/data/issue/1" 544 "link": "https://.../rest/data/issue/1"
506 }, 545 },
507 { 546 {
508 "id": "100", 547 "id": "100",
509 "link": "https://.../rest/data/issue/100" 548 "link": "https://.../rest/data/issue/100"
510 } 549 }
511 ... 550 ...
512 ], 551 ],
513 "@total_size": 171 552 "@total_size": 171
514 } 553 }
515 } 554 }
516 555
517 Collection endpoints support a number of features as seen in the next 556 Collection endpoints support a number of features as seen in the next
518 sections. 557 sections.
519 558
520 A server may implement a default maximum number of items in the 559 Having an empty ``collection`` does not mean next next link will not
521 collection. This can be used to prevent denial of service (DOS). As 560 return more data. The row limit is applied when the query is made to
522 a result all clients must be programmed to expect pagination 561 the database. The result set is then filtered, removing rows that the
523 decorations in the response. See the section on pagination below for 562 user does not have permission to access. So it is possible to have no
524 details. 563 data items on a page because the user does not have access to them. If
564 you use ``@page_size`` near the administrative limit, you may receive
565 fewer rows than requested. However, this does not mean you are out of
566 data.
567
568 All clients must be programmed to expect pagination decorations in the
569 response. See the section on pagination below for details.
525 570
526 Searching 571 Searching
527 ~~~~~~~~~ 572 ~~~~~~~~~
528 573
529 Searching is done by adding roundup field names and values as query 574 Searching is done by adding roundup field names and values as query
530 parameters. Using: https://.../rest/data/issue you can search using: 575 parameters. Using: https://.../rest/data/issue you can search using:
531 576
532 .. list-table:: Query Parameters Examples 577 .. list-table:: Query Parameters Examples
533 :header-rows: 1 578 :header-rows: 1
534 :widths: 20 20 80 579 :widths: 20 20 80
580 :class: valign-top
535 581
536 * - Query parameter 582 * - Query parameter
537 - Field type 583 - Field type
538 - Explanation 584 - Explanation
539 * - ``title=foo`` 585 * - ``title=foo``
569 Searching for strings (e.g. the issue title, or a keyword name) 615 Searching for strings (e.g. the issue title, or a keyword name)
570 performs a case-insensitive substring search. Searching for 616 performs a case-insensitive substring search. Searching for
571 ``title=Something`` (or in long form title~=Something) will find all 617 ``title=Something`` (or in long form title~=Something) will find all
572 issues with "Something" or "someThing", etc. in the title. 618 issues with "Something" or "someThing", etc. in the title.
573 619
574 Changing the search to ``title:=Something`` (note the `:`) performs an 620 Changing the search to ``title:=Something`` (note the ``:``) performs an
575 exact case-sensitive string match for exactly one word ``Something`` 621 exact case-sensitive string match for exactly one word ``Something``
576 with a capital ``S``. Another example is: 622 with a capital ``S``. Another example is:
577 ``title:=test+that+nosy+actually+works.`` where the + signs are spaces 623 ``title:=test+that+nosy+actually+works.`` where the + signs are spaces
578 in the string. Replacing ``+`` with the `URL encoding`_ for space 624 in the string. Replacing ``+`` with the `URL encoding`_ for space
579 ``%20`` will also work. Note that you must match the spaces when 625 ``%20`` will also work. Note that you must match the spaces when
580 performing exact matches. So `title:=test++that+nosy+actually+works.`` 626 performing exact matches. So ``title:=test++that+nosy+actually+works.``
581 matches the word ``test`` with two spaces bewteen ``test`` and 627 matches the word ``test`` with two spaces between ``test`` and
582 ``that`` in the title. 628 ``that`` in the title.
583 629
584 To make this clear, searching 630 To make this clear, searching
585 ``https://.../rest/data/issue?keyword=Foo`` will not work unless there 631 ``https://.../rest/data/issue?keyword=Foo`` will not work unless there
586 is a keyword with a (case sensitive) name field of ``Foo`` which is 632 is a keyword with a (case sensitive) name field of ``Foo`` which is
588 ``name`` using ``https://.../rest/data/keyword?name=Foo`` (note 634 ``name`` using ``https://.../rest/data/keyword?name=Foo`` (note
589 searching keyword class not issue class) will return matches for 635 searching keyword class not issue class) will return matches for
590 ``Foo``, ``foobar``, ``foo taz`` etc. 636 ``Foo``, ``foobar``, ``foo taz`` etc.
591 637
592 In all cases the field ``@total_size`` is reported which is the total 638 In all cases the field ``@total_size`` is reported which is the total
593 number of items available if you were to retrieve all of them. 639 number of items available if you were to retrieve all of them. See
640 more details in the parent section about ``@total_size`` and when it
641 can return ``-1``.
594 642
595 Other data types: Date, Interval, Integer, Number need examples and may 643 Other data types: Date, Interval, Integer, Number need examples and may
596 need work to allow range searches. Full text search (e.g. over the 644 need work to allow range searches. Full text search (e.g. over the
597 body of a msg) is a work in progress. 645 body of a msg) is a work in progress.
598 646
632 would sort by status (in ascending order of the status.order property) 680 would sort by status (in ascending order of the status.order property)
633 and then by id of an issue:: 681 and then by id of an issue::
634 682
635 @sort=status,-id 683 @sort=status,-id
636 684
685 Grouping
686 ~~~~~~~~
687
688 Collection endpoints support grouping. This is controlled by
689 specifying a ``@group`` parameter with a list of properties of
690 the searched class. Optionally properties can include a sign
691 ('+' or '-') to specify the groups are sorted in ascending or
692 descending order, respectively. If no sign is given, the groups
693 are returned in ascending order. The following example would
694 return the issues grouped by status (in order from
695 unread->reolved) then within each status, by priority in
696 descending order (wish -> critical)::
697
698 @group=status,-priority
699
700 Adding ``@fields=status,priority`` to the query will allow you to see
701 the status and priority values change so you can identify the items in
702 each group.
703
704 If combined with ``@sort=-id`` within each group he items would be
705 sorted in descending order by id.
706
707 This is useful for select elements that use optgroup.
637 708
638 Pagination 709 Pagination
639 ~~~~~~~~~~ 710 ~~~~~~~~~~
640 711
641 Collection endpoints support pagination. This is controlled by query 712 Collection endpoints support pagination. This is controlled by query
642 parameters ``@page_size`` and ``@page_index`` (Note the use of the 713 parameters ``@page_size`` and ``@page_index`` (Note the use of the
643 leading `@` to make the parameters distinguishable from field names.) 714 leading ``@`` to make the parameters distinguishable from field names.)
644 715
645 .. list-table:: Query Parameters Examples 716 .. list-table:: Query Parameters Examples
646 :header-rows: 1 717 :header-rows: 1
647 :widths: 20 80 718 :widths: 20 80
719 :class: valign-top
648 720
649 * - Query parameter 721 * - Query parameter
650 - Explanation 722 - Explanation
651 * - ``@page_size`` 723 * - ``@page_size``
652 - specifies how many items are displayed at once. If no 724 - specifies how many items are displayed at once. If no
657 729
658 Also when pagination is enabled the returned data include pagination 730 Also when pagination is enabled the returned data include pagination
659 links along side the collection data. This looks like:: 731 links along side the collection data. This looks like::
660 732
661 { "data": 733 { "data":
662 { 734 {
663 "collection": { ... }, 735 "collection": { ... },
664 "@total_size": 222, 736 "@total_size": 222,
665 "@links": { 737 "@links": {
666 "self": [ 738 "self": [
667 { 739 {
668 "uri": 740 "uri":
669 "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5", 741 "https://.../rest/data/issue?@page_index=1&@fields=status&@page_size=5",
670 "rel": "self" 742 "rel": "self"
671 } 743 }
672 ], 744 ],
673 "next": [ 745 "next": [
674 { 746 {
675 "uri": 747 "uri":
676 "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5", 748 "https://.../rest/data/issue?@page_index=2&@fields=status&@page_size=5",
677 "rel": "next" 749 "rel": "next"
678 } 750 }
679 ] 751 ]
680 } 752 }
681 } 753 }
682 } 754 }
683 755
684 The ``@links`` parameter is a dictionary indexed by 756 The ``@links`` parameter is a dictionary indexed by
685 relationships. Each relationship is a list of one or more full link 757 relationships. Each relationship is a list of one or more full link
703 operation on ``https://.../rest/data/issue``. 775 operation on ``https://.../rest/data/issue``.
704 776
705 .. list-table:: Query Parameters Examples 777 .. list-table:: Query Parameters Examples
706 :header-rows: 1 778 :header-rows: 1
707 :widths: 20 80 779 :widths: 20 80
780 :class: valign-top
708 781
709 * - Query parameter 782 * - Query parameter
710 - Explanation 783 - Explanation
711 * - ``@verbose=0`` 784 * - ``@verbose=0``
712 - each item in the collection has its "id" property displayed 785 - each item in the collection has its "id" property displayed
715 - for collections this output is the same as ``@verbose=0``. This 788 - for collections this output is the same as ``@verbose=0``. This
716 is the default. 789 is the default.
717 * - ``@verbose=2`` 790 * - ``@verbose=2``
718 - each item in the collection includes the "label" property in 791 - each item in the collection includes the "label" property in
719 addition to "id" property and a link for the item. 792 addition to "id" property and a link for the item.
720 This is useful as documented below in "Searches and selection"_. 793 This is useful as documented below in `Searches and selection`_.
721 * - ``@verbose=3`` 794 * - ``@verbose=3``
722 - will display the content property of messages and files. Note 795 - will display the content property of messages and files. Note
723 warnings about this below. Using this for collections is 796 warnings about this below. Using this for collections is
724 discouraged as it is slow and produces a lot of data. 797 discouraged as it is slow and produces a lot of data.
725 * - ``@fields=status,title`` 798 * - ``@fields=status,title``
742 ``https://.../rest/data/issue?@verbose=2&@fields=status`` returns:: 815 ``https://.../rest/data/issue?@verbose=2&@fields=status`` returns::
743 816
744 817
745 { 818 {
746 "data": { 819 "data": {
747 "collection": [ 820 "collection": [
748 { 821 {
749 "link": "https://.../rest/data/issue/1", 822 "link": "https://.../rest/data/issue/1",
750 "title": "Welcome to the tracker START HERE", 823 "title": "Welcome to the tracker START HERE",
751 "id": "1", 824 "id": "1",
752 "status": { 825 "status": {
753 "link": "https://.../rest/data/status/1", 826 "link": "https://.../rest/data/status/1",
754 "id": "1", 827 "id": "1",
755 "name": "new" 828 "name": "new"
756 } 829 }
757 }, 830 },
758 ... 831 ...
759 } 832 }
760 833
761 the format of the status field (included because of 834 the format of the status field (included because of
762 ``@fields=status``) includes the label for the status. This is due to 835 ``@fields=status``) includes the label for the status. This is due to
763 inclusion of ``@verbose=2``. Without verbose you would see:: 836 inclusion of ``@verbose=2``. Without verbose you would see::
764 837
765 { 838 {
766 "data": { 839 "data": {
767 "collection": [ 840 "collection": [
768 { 841 {
769 "link": "https://.../rest/data/issue/1", 842 "link": "https://.../rest/data/issue/1",
770 "id": "1", 843 "id": "1",
771 "status": { 844 "status": {
772 "link": "https://.../rest/data/status/1", 845 "link": "https://.../rest/data/status/1",
773 "id": "1" 846 "id": "1"
774 } 847 }
775 }, 848 },
776 ... 849 ...
777 } 850 }
778 851
779 Note that the ``link`` field that is returned doesn't exist in the 852 Note that the ``link`` field that is returned doesn't exist in the
780 database. It is a construct of the rest interface. This means that you 853 database. It is a construct of the rest interface. This means that you
796 { 869 {
797 "data": { 870 "data": {
798 "id": "11", 871 "id": "11",
799 "type": "msg", 872 "type": "msg",
800 "link": "https://.../demo/rest/data/msg/11", 873 "link": "https://.../demo/rest/data/msg/11",
801 "attributes": { 874 "attributes": {
802 "author": { 875 "author": {
803 "id": "5", 876 "id": "5",
804 "link": "https://.../demo/rest/data/user/5" 877 "link": "https://.../demo/rest/data/user/5"
805 }, 878 },
806 "content": { 879 "content": {
833 }, 906 },
834 "@etag": "\"584f82231079e349031bbb853747df1c\"" 907 "@etag": "\"584f82231079e349031bbb853747df1c\""
835 } 908 }
836 } 909 }
837 910
911 With Roundup 2.5 you can retrieve the data directly from the rest
912 interface using the ``Accept`` header value to select a structured (json
913 or optional xml) representation (as above) or a stream with just the
914 content data.
915
916 Using the wildcard type ``*/*`` in the ``Accept`` header with the url
917 ``.../binary_content`` will return the raw data and the recorded mime
918 type of the the data as the ``Content-Type``. Using ``*/*`` with
919 another end point will return ``json`` data. An ``Accept`` value of
920 ``application/octet-stream`` matches any mime type and retrieves the
921 raw data as ``Content-Type: application/octet-stream``.
922
923 To access the contents of a PNG image file (in file23), you use the
924 following link:
925 ``https://.../demo/rest/data/file/23/binary_content``. To find out the
926 mime type, you can check this URL:
927 ``https://.../demo/rest/data/file/23/type``.
928
929 By setting the header to ``Accept: application/octet-stream; q=1.0,
930 application/json; q=0.5``, you will receive the binary PNG file with
931 the header ``Content-Type: application/octet-stream``. If you switch
932 the ``q`` values, you will receive the encoded JSON version::
933
934 {
935 "data": {
936 "id": "23",
937 "type": "<class 'bytes'>",
938 "link": "https://.../demo/rest/data/file/23/binary_content",
939 "data": "b'\\x89PNG\\r\\n\\x1a\\n\\x00[...]0\\x00\\x00\\x00IEND\\xaeB`\\x82'",
940 "@etag": "\"db6adc1b09d95b0388d79c7905bc7982\""
941 }
942 }
943
944 with ``Content-Type: application/json`` and a (4x larger) json encoded
945 representation of the binary data.
946
947 If you want it returned with a ``Content-Type: image/png`` header,
948 you can use ``image/png`` or ``*/*`` in the Accept header.
949
950 For message files, you can use
951 ``https://.../demo/rest/data/msg/23/binary_content`` with ``Accept:
952 application/octet-stream; q=0.5, application/json; q=0.4, image/png;
953 q=0.495, text/*``. It will return the plain text of the message.
954
955 Most message files are not stored with a mime type. Getting
956 ``https://.../demo/rest/data/msg/23/type`` returns::
957
958 {
959 "data": {
960 "id": "23",
961 "type": "<class 'NoneType'>",
962 "link": "https://.../demo/rest/data/msg/23/type",
963 "data": null,
964 "@etag": "\"ba98927a8bb4c56f6cfc31a36f94ad16\""
965 }
966 }
967
968 The data attribute will usually be null/empty. As a result, mime type
969 matching for an item without a mime type is forgiving.
970
971 Messages are meant to be human readable, so the mime type ``text/*``
972 can be used to access any text style mime type (``text/plain``,
973 ``text/x-rst``, ``text/markdown``, ``text/html``, ...) or an empty
974 mime type. If the item's type is not empty, it will be used as the
975 Content-Type (similar to ``*/*``). Otherwise ``text/*`` will be the
976 Content-Type. If your tracker supports markup languages
977 (e.g. markdown), you should set the mime type (e.g. ``text/markdown``)
978 when storing your message.
979
980 Note that the header ``X-Content-Type-Options: nosniff`` is returned
981 with a non javascript or xml binary_content response to prevent the
982 browser from trying to interpret the returned data.
983
984 Legacy Method (HTML interface)
985 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
986
987 With the addition of file binary content streaming in the rest
988 interface to Roundup 2.5.0, this method (using the html interface) is
989 considered legacy but still works.
990
838 To retreive the content, you can use the content link property: 991 To retreive the content, you can use the content link property:
839 ``https://.../demo/msg11/``. The trailing / is required. Without the 992 ``https://.../demo/msg11/``. The trailing / is required. Without the
840 /, you get a web page that includes metadata about the message. With 993 /, you get a web page that includes metadata about the message. With
841 the slash you get a text/plain (in most cases) data stream. 994 the slash you get a text/plain (in most cases) data stream.
842 995
853 reprehenderit\neu to quisquam velit, passage, 1006 reprehenderit\neu to quisquam velit, passage,
854 was or toil BC quis denouncing quia\nexercise, 1007 was or toil BC quis denouncing quia\nexercise,
855 veritatis et used voluptas I elit, a The...", 1008 veritatis et used voluptas I elit, a The...",
856 "date": "2017-10-30.00:53:15", 1009 "date": "2017-10-30.00:53:15",
857 ... 1010 ...
858 1011
859 Lines are wrapped for display, content value is one really long 1012 Lines are wrapped for display, content value is one really long
860 line. If the data is not utf-8 compatible, you will get a link. 1013 line. If the data is not utf-8 compatible, you will get a link.
861 1014
862 Retrieving the contents of a file is similar. Performing a 1015 Retrieving the contents of a file is similar. Performing a
863 get on ``https://.../demo/rest/data/file/11`` returns:: 1016 get on ``https://.../demo/rest/data/file/11`` returns::
892 ``https://.../demo/rest/data/file/11?@verbose=3`` the content field 1045 ``https://.../demo/rest/data/file/11?@verbose=3`` the content field
893 above is displayed as (wrapped for display):: 1046 above is displayed as (wrapped for display)::
894 1047
895 "content": "file11 is not text, retrieve using binary_content 1048 "content": "file11 is not text, retrieve using binary_content
896 property. mdsum: bd990c0f8833dd991daf610b81b62316", 1049 property. mdsum: bd990c0f8833dd991daf610b81b62316",
897
898 1050
899 You can use the `binary_content property`_ described below to 1051 You can use the `binary_content property`_ described below to
900 retrieve an encoded copy of the data. 1052 retrieve an encoded copy of the data.
901 1053
902 Other query params 1054 Other query params
905 This table lists other supported parameters: 1057 This table lists other supported parameters:
906 1058
907 .. list-table:: Query Parameters Examples 1059 .. list-table:: Query Parameters Examples
908 :header-rows: 1 1060 :header-rows: 1
909 :widths: 20 80 1061 :widths: 20 80
1062 :class: valign-top
910 1063
911 * - Query parameter 1064 * - Query parameter
912 - Explanation 1065 - Explanation
913 * - ``@pretty=false`` 1066 * - ``@pretty=false``
914 - by default json data is pretty printed to make it readable to 1067 - by default json data is pretty printed to make it readable to
925 of a class, e.g., a new issue via the ``/data/issue`` link. The post 1078 of a class, e.g., a new issue via the ``/data/issue`` link. The post
926 gets a dictionary of keys/values for the new item. It returns the same 1079 gets a dictionary of keys/values for the new item. It returns the same
927 parameters as the GET method after successful creation. 1080 parameters as the GET method after successful creation.
928 1081
929 If you perform a get on an item with ``@verbose=0``, it is in the 1082 If you perform a get on an item with ``@verbose=0``, it is in the
930 correct form to use as a the payload of a post. 1083 correct form to use as the payload of a post.
931 1084
932 1085
933 Safely Re-sending POST 1086 Safely Re-sending POST
934 ^^^^^^^^^^^^^^^^^^^^^^ 1087 ^^^^^^^^^^^^^^^^^^^^^^
935 1088
1016 to create a user. Creating generic POE tokens is *not* recommended, 1169 to create a user. Creating generic POE tokens is *not* recommended,
1017 but is available if a use case requires it. 1170 but is available if a use case requires it.
1018 1171
1019 This example also changes the lifetime of the POE url. This link has 1172 This example also changes the lifetime of the POE url. This link has
1020 a lifetime of 15 minutes (900 seconds). Using it after 16 minutes will 1173 a lifetime of 15 minutes (900 seconds). Using it after 16 minutes will
1021 result in a 400 error. A lifetime up to 1 hour can be specified. 1174 result in a 400 error. A lifetime up to 3600 seconds (1 hour) can be
1175 specified.
1022 1176
1023 POE url's are an optional mechanism. If: 1177 POE url's are an optional mechanism. If:
1024 1178
1025 * you do not expect your client to retry a failed post, 1179 * you do not expect your client to retry a failed post,
1026 * a failed post is unlikely (e.g. you are running over a local lan), 1180 * a failed post is unlikely (e.g. you are running over a local lan),
1047 Supports the ``OPTIONS`` method for determining which methods are 1201 Supports the ``OPTIONS`` method for determining which methods are
1048 allowed on a given endpoint. 1202 allowed on a given endpoint.
1049 1203
1050 Does not support PUT, DELETE or PATCH. 1204 Does not support PUT, DELETE or PATCH.
1051 1205
1206 /data/user/roles endpoint
1207 -------------------------
1208
1209 The list of valid roles for a user is not an actual class in the
1210 hyperdb. This endpoint returns a list of all defined roles if the
1211 user has the ``Admin`` role. Otherwise it returns a 403 - not
1212 authorized error. The output from this endpoint looks like::
1213
1214 {
1215 "data": {
1216 "collection": [
1217 {
1218 "id": "user",
1219 "name": "user"
1220 },
1221 {
1222 "id": "admin",
1223 "name": "admin"
1224 },
1225 {
1226 "id": "anonymous",
1227 "name": "anonymous"
1228 }
1229 ]
1230 }
1231 }
1232
1233 to mimic a class collection.
1234
1235 Unlike a real class collection endpoint, ``@total_size`` is not
1236 returned. Also it does not support and ignores any query options like:
1237 filtering, ``@sort``, ``@group``, ``@verbose`` etc. Note that the ``id``
1238 property is not numeric.
1239
1240 This endpoint was introduced in release 2.4.0 to support a roles
1241 select/dropdown in the web component classhelper. This lets the web
1242 component helper implement the same function in the classic user class
1243 classhelper.
1244
1052 /data/\ *class*/\ *id* item 1245 /data/\ *class*/\ *id* item
1053 --------------------------- 1246 ---------------------------
1054 1247
1055 When performing the ``GET`` method on an item 1248 When you use the ``GET`` method on an item
1056 (e.g. ``/data/issue/42``), a ``link`` attribute contains the link to 1249 (e.g. ``/data/issue/42``), a ``link`` attribute contains the link to
1057 the item, ``id`` contains the id, ``type`` contains the class name 1250 the item, ``id`` contains the id, ``type`` contains the class name
1058 (e.g. ``issue`` in the example) and an ``etag`` property can be used 1251 (e.g. ``issue`` in the example) and an ``etag`` property can be used
1059 to detect modifications since the last query. 1252 to detect modifications since the last query.
1060 1253
1091 1284
1092 An example of returned values:: 1285 An example of returned values::
1093 1286
1094 { 1287 {
1095 "data": { 1288 "data": {
1096 "type": "issue", 1289 "type": "issue",
1097 "@etag": "\"f15e6942f00a41960de45f9413684591\"", 1290 "@etag": "\"f15e6942f00a41960de45f9413684591\"",
1098 "link": "https://.../rest/data/issue/23", 1291 "link": "https://.../rest/data/issue/23",
1099 "attributes": { 1292 "attributes": {
1100 "keyword": [], 1293 "keyword": [],
1101 "messages": [ 1294 "messages": [
1102 { 1295 {
1103 "link": "https://.../rest/data/msg/375", 1296 "link": "https://.../rest/data/msg/375",
1104 "id": "375" 1297 "id": "375"
1105 }, 1298 },
1106 { 1299 {
1107 "link": "https://.../rest/data/msg/376", 1300 "link": "https://.../rest/data/msg/376",
1108 "id": "376" 1301 "id": "376"
1109 }, 1302 },
1110 ... 1303 ...
1111 ], 1304 ],
1112 "files": [], 1305 "files": [],
1113 "status": { 1306 "status": {
1114 "link": "https://.../rest/data/status/2", 1307 "link": "https://.../rest/data/status/2",
1115 "id": "2" 1308 "id": "2"
1116 }, 1309 },
1117 "title": "This is a title title", 1310 "title": "This is a title title",
1118 "superseder": [], 1311 "superseder": [],
1119 "nosy": [ 1312 "nosy": [
1120 { 1313 {
1121 "link": "https://.../rest/data/user/4", 1314 "link": "https://.../rest/data/user/4",
1122 "id": "4" 1315 "id": "4"
1123 }, 1316 },
1124 { 1317 {
1125 "link": "https://.../rest/data/user/5", 1318 "link": "https://.../rest/data/user/5",
1126 "id": "5" 1319 "id": "5"
1127 } 1320 }
1128 ], 1321 ],
1129 "assignedto": null, 1322 "assignedto": null,
1130 }, 1323 },
1131 "id": "23" 1324 "id": "23"
1132 } 1325 }
1133 } 1326 }
1134 1327
1135 Retrieve item using key value 1328 Retrieve item using key value
1136 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1329 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1192 "data": { 1385 "data": {
1193 "id": "12", 1386 "id": "12",
1194 "link": "https://.../demo/rest/data/file/12" 1387 "link": "https://.../demo/rest/data/file/12"
1195 } 1388 }
1196 } 1389 }
1197 1390
1198 1391
1199 Other Supported Methods for Items 1392 Other Supported Methods for Items
1200 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1393 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1201 1394
1202 The method ``PUT`` is allowed on individual items, e.g. 1395 The method ``PUT`` is allowed on individual items, e.g.
1203 ``/data/issue/42`` On success it returns the same parameters as the 1396 ``/data/issue/42`` On success it returns a data structure similar to
1204 respective ``GET`` method. Note that for ``PUT`` an Etag has to be 1397 the respective ``GET`` method. However it is only concerned with the
1205 supplied, either in the request header or as an @etag parameter. An 1398 changes that have occurred. Since it is not a full ``GET`` request, it
1206 example:: 1399 doesn't include an etag or unchanged attributes. Note that for ``PUT``
1400 an Etag has to be supplied, either in the request header or as an
1401 @etag parameter. An example::
1207 1402
1208 curl -u admin:admin -X PUT \ 1403 curl -u admin:admin -X PUT \
1209 --header 'Referer: https://example.com/demo/' \ 1404 --header 'Referer: https://example.com/demo/' \
1210 --header 'X-Requested-With: rest' \ 1405 --header 'X-Requested-With: rest' \
1211 --header "Content-Type: application/json" \ 1406 --header "Content-Type: application/json" \
1214 --data '{ "nosy": [ "1", "5" ] }' \ 1409 --data '{ "nosy": [ "1", "5" ] }' \
1215 "https://example.com/demo/rest/data/issue/23" 1410 "https://example.com/demo/rest/data/issue/23"
1216 1411
1217 { 1412 {
1218 "data": { 1413 "data": {
1219 "attribute": { 1414 "attribute": {
1220 "nosy": [ 1415 "nosy": [
1221 "1", 1416 "1",
1222 "5" 1417 "5"
1223 ] 1418 ]
1224 }, 1419 },
1225 "type": "issue", 1420 "type": "issue",
1226 "link": "https://example.com/demo/rest/data/issue/23", 1421 "link": "https://example.com/demo/rest/data/issue/23",
1227 "id": "23" 1422 "id": "23"
1228 } 1423 }
1229 } 1424 }
1230 1425
1231 If the above command is repeated with the data attribute:: 1426 If the above command is repeated with the data attribute::
1232 1427
1234 1429
1235 this is returned:: 1430 this is returned::
1236 1431
1237 { 1432 {
1238 "data": { 1433 "data": {
1239 "attribute": { 1434 "attribute": {
1240 "title": "This is now my title" 1435 "title": "This is now my title"
1241 }, 1436 },
1242 "type": "issue", 1437 "type": "issue",
1243 "link": 1438 "link":
1244 "https://.../demo/rest/data/issue/23", 1439 "https://.../demo/rest/data/issue/23",
1245 "id": "23" 1440 "id": "23"
1246 } 1441 }
1247 } 1442 }
1248 1443
1249 Note that nosy is not in the attributes returned. It is the same as 1444 Note that nosy is not in the attributes returned. It is the same as
1250 before, so no change has happened and it is not reported. 1445 before, so no change has happened and it is not reported.
1260 "https://.../demo/rest/data/issue/23" 1455 "https://.../demo/rest/data/issue/23"
1261 1456
1262 which returns both title and nosy attributes:: 1457 which returns both title and nosy attributes::
1263 1458
1264 { 1459 {
1265 "data": { 1460 "data": {
1266 "attribute": { 1461 "attribute": {
1267 "title": "This is now my new title", 1462 "title": "This is now my new title",
1268 "nosy": [ 1463 "nosy": [
1269 "4", 1464 "4",
1270 "5" 1465 "5"
1271 ] 1466 ]
1272 }, 1467 },
1273 "type": "issue", 1468 "type": "issue",
1274 "link": 1469 "link":
1275 "https://.../demo/rest/data/issue/23", 1470 "https://.../demo/rest/data/issue/23",
1276 "id": "23" 1471 "id": "23"
1277 } 1472 }
1278 } 1473 }
1279 1474
1280 Note that mixing url query parameters with payload submission doesn't 1475 Note that mixing url query parameters with payload submission doesn't
1281 work. So using:: 1476 work. So using::
1282 1477
1326 1521
1327 which returns:: 1522 which returns::
1328 1523
1329 { 1524 {
1330 "data": { 1525 "data": {
1331 "attribute": { 1526 "attribute": {
1332 "nosy": [ 1527 "nosy": [
1333 "3", 1528 "3",
1334 "4" 1529 "4"
1335 ] 1530 ]
1336 }, 1531 },
1337 "type": "issue", 1532 "type": "issue",
1338 "link": "https://.../rest/data/issue/23", 1533 "link": "https://.../rest/data/issue/23",
1339 "id": "23" 1534 "id": "23"
1340 } 1535 }
1341 } 1536 }
1342 1537
1343 Note that the changed values are returned so you can update 1538 Note that the changed values are returned so you can update
1344 internal state in your app with the new data. 1539 internal state in your app with the new data.
1360 1555
1361 For example:: 1556 For example::
1362 1557
1363 { 1558 {
1364 "data": { 1559 "data": {
1365 "link": "https://.../rest/data/issue/22/title", 1560 "link": "https://.../rest/data/issue/22/title",
1366 "data": "I need Broken PC", 1561 "data": "I need Broken PC",
1367 "type": "<class 'str'>", 1562 "type": "<class 'str'>",
1368 "id": "22", 1563 "id": "22",
1369 "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\"" 1564 "@etag": "\"370510512b2d8fc3f98aac3d762cc7b1\""
1370 } 1565 }
1371 } 1566 }
1372 1567
1373 1568
1374 All endpoints support an ``OPTIONS`` method for determining which 1569 All endpoints support an ``OPTIONS`` method for determining which
1388 "link": "https://.../demo/rest/data/msg/11/content", 1583 "link": "https://.../demo/rest/data/msg/11/content",
1389 "data": "of has to who pleasure. or of account give because the 1584 "data": "of has to who pleasure. or of account give because the
1390 reprehenderit\neu to quisquam velit, passage, was or...", 1585 reprehenderit\neu to quisquam velit, passage, was or...",
1391 "@etag": "\"584f82231079e349031bbb853747df1c\"" 1586 "@etag": "\"584f82231079e349031bbb853747df1c\""
1392 } 1587 }
1393 } 1588 }
1394 1589
1395 (the content property is wrapped for display, it is one long line.) 1590 (the content property is wrapped for display, it is one long line.)
1396 1591
1397 .. _binary_content property: 1592 .. _binary_content property:
1398 1593
1421 Other Supported Methods for fields 1616 Other Supported Methods for fields
1422 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1617 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1423 1618
1424 The method ``PUT`` is allowed on a property e.g., 1619 The method ``PUT`` is allowed on a property e.g.,
1425 ``/data/issue/42/title``. On success it returns the same parameters as 1620 ``/data/issue/42/title``. On success it returns the same parameters as
1426 the respective ``GET`` method. Note that for ``PUT`` an Etag has to be 1621 the respective ``PUT`` method on the item. For example::
1622
1623 {
1624 "data": {
1625 "id": "42",
1626 "type": "issue",
1627 "link": "https://.../demo/rest/data/issue/42",
1628 "attribute": {
1629 "title": "this is a new title"
1630 }
1631 }
1632 }
1633
1634 If the new value for the title was the same as on the server, the
1635 attribute property would be empty since the value was not changed.
1636 Note that for ``PUT`` an Etag has to be
1427 supplied, either in the request header or as an @etag parameter. 1637 supplied, either in the request header or as an @etag parameter.
1428 Example using multipart/form-data rather than json:: 1638 Example using multipart/form-data rather than json::
1429 1639
1430 curl -vs -u provisional:provisional -X PUT \ 1640 curl -vs -u provisional:provisional -X PUT \
1431 --header "Accept: application/json" \ 1641 --header "Accept: application/json" \
1491 1701
1492 1702
1493 >>> import requests 1703 >>> import requests
1494 >>> u = 'http://user:password@tracker.example.com/demo/rest/data/' 1704 >>> u = 'http://user:password@tracker.example.com/demo/rest/data/'
1495 >>> s = requests.session() 1705 >>> s = requests.session()
1496 >>> session.auth = ('admin', 'admin') 1706 >>> session.auth = ('admin', 'admin')
1497 >>> r = s.get(u + 'issue/42/title') 1707 >>> r = s.get(u + 'issue/42/title')
1498 >>> if r.status_code != 200: 1708 >>> if r.status_code != 200:
1499 ... print("Failed: %s: %s" % (r.status_code, r.reason)) 1709 ... print("Failed: %s: %s" % (r.status_code, r.reason))
1500 ... exit(1) 1710 ... exit(1)
1501 >>> print (r.json() ['data']['data'] 1711 >>> print (r.json() ['data']['data']
1513 >>> r = s.get (u + 'issue/42') 1723 >>> r = s.get (u + 'issue/42')
1514 >>> etag = r.headers['ETag'] 1724 >>> etag = r.headers['ETag']
1515 >>> print("ETag: %s" % etag) 1725 >>> print("ETag: %s" % etag)
1516 >>> etag = r.json()['data']['@etag'] 1726 >>> etag = r.json()['data']['@etag']
1517 >>> print("@etag: %s" % etag) 1727 >>> print("@etag: %s" % etag)
1518 >>> h = {'If-Match': etag, 1728 >>> h = {'If-Match': etag,
1519 ... 'X-Requested-With': 'rest', 1729 ... 'X-Requested-With': 'rest',
1520 ... 'Referer': 'http://tracker.example.com/demo/'} 1730 ... 'Referer': 'http://tracker.example.com/demo/'}
1521 >>> d = {'@op:'action', '@action_name':'retire'} 1731 >>> d = {'@op:'action', '@action_name':'retire'}
1522 >>> r = s.patch(u + 'issue/42', data = d, headers = h) 1732 >>> r = s.patch(u + 'issue/42', data = d, headers = h)
1523 >>> print(r.json()) 1733 >>> print(r.json())
1535 1745
1536 A similar curl based retire example is to use:: 1746 A similar curl based retire example is to use::
1537 1747
1538 curl -s -u admin:admin \ 1748 curl -s -u admin:admin \
1539 -H "Referer: https://tracker.example.com/demo/" \ 1749 -H "Referer: https://tracker.example.com/demo/" \
1540 -H "X-requested-with: rest" \ 1750 -H "X-requested-with: rest" \
1541 -H "Content-Type: application/json" \ 1751 -H "Content-Type: application/json" \
1542 https://tracker.example.com/demo/rest/data/status/1 1752 https://tracker.example.com/demo/rest/data/status/1
1543 1753
1544 to get the etag manually. Then insert the etag in the If-Match header 1754 to get the etag manually. Then insert the etag in the If-Match header
1545 for this retire example:: 1755 for this retire example::
1546 1756
1547 curl -s -u admin:admin \ 1757 curl -s -u admin:admin \
1548 -H "Referer: https://tracker.example.com/demo/" \ 1758 -H "Referer: https://tracker.example.com/demo/" \
1549 -H "X-requested-with: rest" \ 1759 -H "X-requested-with: rest" \
1550 -H "Content-Type: application/json" \ 1760 -H "Content-Type: application/json" \
1551 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \ 1761 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
1552 --data-raw '{ "@op":"action", "@action_name": "retire" }'\ 1762 --data-raw '{ "@op":"action", "@action_name": "retire" }'\
1553 -X PATCH \ 1763 -X PATCH \
1554 https://tracker.example.com/demo/rest/data/status/1 1764 https://tracker.example.com/demo/rest/data/status/1
1555 1765
1556 and restore:: 1766 and restore::
1557 1767
1558 curl -s -u admin:admin \ 1768 curl -s -u admin:admin \
1559 -H "Referer: https://tracker.example.com/demo/" \ 1769 -H "Referer: https://tracker.example.com/demo/" \
1560 -H "X-requested-with: rest" \ 1770 -H "X-requested-with: rest" \
1561 -H "Content-Type: application/json" \ 1771 -H "Content-Type: application/json" \
1562 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \ 1772 -H 'If-Match: "a502faf4d6b8e3897c4ecd66b5597571"' \
1563 --data-raw '{ "@op":"action", "@action_name": "restore" }'\ 1773 --data-raw '{ "@op":"action", "@action_name": "restore" }'\
1564 -X PATCH \ 1774 -X PATCH \
1565 https://tracker.example.com/demo/rest/data/status/1 1775 https://tracker.example.com/demo/rest/data/status/1
1566 1776
1567 1777
1568 Searches and selection 1778 Searches and selection
1569 ---------------------- 1779 ----------------------
1570 1780
1579 1789
1580 Consider a multi-select box for the superseder property. Using 1790 Consider a multi-select box for the superseder property. Using
1581 selectize.js (and jquery) code similar to:: 1791 selectize.js (and jquery) code similar to::
1582 1792
1583 $('#superseder').selectize({ 1793 $('#superseder').selectize({
1584 valueField: 'id', 1794 valueField: 'id',
1585 labelField: 'title', 1795 labelField: 'title',
1586 searchField: 'title', ... 1796 searchField: 'title', ...
1587 load: function(query, callback) { 1797 load: function(query, callback) {
1588 if (!query.length) return callback(); 1798 if (!query.length) return callback();
1589 $.ajax({ 1799 $.ajax({
1590 url: '.../rest/data/issue?@verbose=2&title=' 1800 url: '.../rest/data/issue?@verbose=2&title='
1591 + encodeURIComponent(query), 1801 + encodeURIComponent(query),
1592 type: 'GET', 1802 type: 'GET',
1593 error: function() {callback();}, 1803 error: function() {callback();},
1594 success: function(res) { 1804 success: function(res) {
1595 callback(res.data.collection);} 1805 callback(res.data.collection);}
1596 1806
1597 Sets up a box that a user can type the word "request" into. Then 1807 Sets up a box that a user can type the word "request" into. Then
1598 selectize.js will use that word to generate an ajax request with the 1808 selectize.js will use that word to generate an ajax request with the
1599 url: ``.../rest/data/issue?@verbose=2&title=request`` 1809 url: ``.../rest/data/issue?@verbose=2&title=request``
1600 1810
1603 { 1813 {
1604 "data": { 1814 "data": {
1605 "@total_size": 440, 1815 "@total_size": 440,
1606 "collection": [ 1816 "collection": [
1607 { 1817 {
1608 "link": ".../rest/data/issue/8", 1818 "link": ".../rest/data/issue/8",
1609 "id": "8", 1819 "id": "8",
1610 "title": "Request for Power plugs" 1820 "title": "Request for Power plugs"
1611 }, 1821 },
1612 { 1822 {
1613 "link": ".../rest/data/issue/27", 1823 "link": ".../rest/data/issue/27",
1614 "id": "27", 1824 "id": "27",
1615 "title": "Request for foo" 1825 "title": "Request for foo"
1616 }, 1826 },
1617 ... 1827 ...
1618 1828
1619 selectize.js will look at these objects (as passed to 1829 selectize.js will look at these objects (as passed to
1620 callback(res.data.collection)) and create a select list from the each 1830 callback(res.data.collection)) and create a select list from the each
1678 from roundup.rest import Routing, RestfulInstance, _data_decorator 1888 from roundup.rest import Routing, RestfulInstance, _data_decorator
1679 from roundup.exceptions import Unauthorised 1889 from roundup.exceptions import Unauthorised
1680 1890
1681 class RestfulInstance: 1891 class RestfulInstance:
1682 1892
1683 @Routing.route("/summary2") 1893 @Routing.route("/summary2")
1684 @_data_decorator 1894 @_data_decorator
1685 def summary2(self, input): 1895 def summary2(self, input):
1686 result = { "hello": "world" } 1896 result = { "hello": "world" }
1687 return 200, result 1897 return 200, result
1688 1898
1689 will make a new endpoint .../rest/summary2 that you can test with:: 1899 will make a new endpoint .../rest/summary2 that you can test with::
1690 1900
1691 $ curl -X GET .../rest/summary2 1901 $ curl -X GET .../rest/summary2
1692 { 1902 {
1698 Similarly appending this to interfaces.py after summary2:: 1908 Similarly appending this to interfaces.py after summary2::
1699 1909
1700 # handle more endpoints 1910 # handle more endpoints
1701 @Routing.route("/data/<:class_name>/@schema", 'GET') 1911 @Routing.route("/data/<:class_name>/@schema", 'GET')
1702 def get_element_schema(self, class_name, input): 1912 def get_element_schema(self, class_name, input):
1703 result = { "schema": {} } 1913 result = { "schema": {} }
1704 uid = self.db.getuid () 1914 uid = self.db.getuid ()
1705 if not self.db.security.hasPermission('View', uid, class_name) : 1915 if not self.db.security.hasPermission('View', uid, class_name) :
1706 raise Unauthorised('Permission to view %s denied' % class_name) 1916 raise Unauthorised('Permission to view %s denied' % class_name)
1707 1917
1708 class_obj = self.db.getclass(class_name) 1918 class_obj = self.db.getclass(class_name)
1709 props = class_obj.getprops(protected=False) 1919 props = class_obj.getprops(protected=False)
1710 schema = result['schema'] 1920 schema = result['schema']
1711 1921
1712 for prop in props: 1922 for prop in props:
1713 schema[prop] = { "type": repr(class_obj.properties[prop]) } 1923 schema[prop] = { "type": repr(class_obj.properties[prop]) }
1714 1924
1715 return result 1925 return result
1716 1926
1717 .. 1927 ..
1718 the # comment in the example is needed to preserve indention under Class. 1928 the # comment in the example is needed to preserve indention under Class.
1719 1929
1720 returns some data about the class:: 1930 returns some data about the class::
1721 1931
1722 $ curl -X GET .../rest/data/issue/@schema 1932 $ curl -X GET .../rest/data/issue/@schema
1723 { 1933 {
1724 "schema": { 1934 "schema": {
1725 "keyword": { 1935 "keyword": {
1726 "type": "<roundup.hyperdb.Multilink to \"keyword\">" 1936 "type": "<roundup.hyperdb.Multilink to \"keyword\">"
1727 }, 1937 },
1728 "title": { 1938 "title": {
1729 "type": "<roundup.hyperdb.String>" 1939 "type": "<roundup.hyperdb.String>"
1730 }, 1940 },
1731 "files": { 1941 "files": {
1732 "type": "<roundup.hyperdb.Multilink to \"file\">" 1942 "type": "<roundup.hyperdb.Multilink to \"file\">"
1733 }, 1943 },
1734 "status": { 1944 "status": {
1735 "type": "<roundup.hyperdb.Link to \"status\">" 1945 "type": "<roundup.hyperdb.Link to \"status\">"
1736 }, ... 1946 }, ...
1737 } 1947 }
1738 } 1948 }
1739 1949
1740 1950
1741 Adding other endpoints (e.g. to allow an OPTIONS query against 1951 Adding other endpoints (e.g. to allow an OPTIONS query against
1742 ``/data/issue/@schema``) is left as an exercise for the reader. 1952 ``/data/issue/@schema``) is left as an exercise for the reader.
1793 However the templating system can access the hyperdb directly which 2003 However the templating system can access the hyperdb directly which
1794 allows filtering to happen with admin privs escaping the standard 2004 allows filtering to happen with admin privs escaping the standard
1795 permissions scheme. For example access to a user's roles should be 2005 permissions scheme. For example access to a user's roles should be
1796 limited to the user (read only) and an admin. If you have customised 2006 limited to the user (read only) and an admin. If you have customised
1797 your schema to implement `Restricting the list of 2007 your schema to implement `Restricting the list of
1798 users that are assignable to a task <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__ 2008 users that are assignable to a task
2009 <customizing.html#restricting-the-list-of-users-that-are-assignable-to-a-task>`__
1799 so that only users with a 2010 so that only users with a
1800 Developer role are allowed to be assigned to an issue, a rest end 2011 Developer role are allowed to be assigned to an issue, a rest end
1801 point must be added to provide a view that exposes users with this 2012 point must be added to provide a view that exposes users with this
1802 permission. 2013 permission.
1803 2014
1804 Using the normal ``/data/user?roles=Developer`` will return all the 2015 Using the normal ``/data/user?roles=Developer`` will return all the
1805 users in the system unless you are an admin user because most users 2016 users in the system unless you are an admin user because most users
1806 can't see the roles. Building on the `Adding new rest endpoints`_ 2017 can't see the roles. Building on the `Adding new rest endpoints`_
1807 section this code adds a new endpoint `/data/@permission/Developer` 2018 section this code adds a new endpoint ``/data/@permission/Developer``
1808 that returns a list of users with the developer role:: 2019 that returns a list of users with the developer role::
1809 2020
1810 from roundup.rest import Routing, RestfulInstance 2021 from roundup.rest import Routing, RestfulInstance
1811 from roundup.anypy.cgi_ import MiniFieldStorage 2022 from roundup.anypy.cgi_ import MiniFieldStorage
1812 2023
1813 class RestfulInstance(object): 2024 class RestfulInstance(object):
1814 2025
1815 @Routing.route("/data/@permission/Developer") 2026 @Routing.route("/data/@permission/Developer")
1816 def get_role_Developer(self, input): 2027 def get_role_Developer(self, input):
1817 '''An endpoint to return a list of users with Developer 2028 '''An endpoint to return a list of users with Developer
1818 role who can be assigned to an issue. 2029 role who can be assigned to an issue.
1819 2030
1820 It ignores attempt to search by any property except 2031 It ignores attempt to search by any property except
1821 username and realname. It also ignores the whole @fields 2032 username and realname. It also ignores the whole @fields
1822 specification if it specifies a property the user 2033 specification if it specifies a property the user
1823 can't view. Other @ query params (e.g. @page... and 2034 can't view. Other @ query params (e.g. @page... and
1824 @verbose) are supported. 2035 @verbose) are supported.
1825 2036
1826 It assumes admin access rights so that the roles property 2037 It assumes admin access rights so that the roles property
1827 of the user can be searched. This is needed if the roles 2038 of the user can be searched. This is needed if the roles
1828 property is not searchable/viewable by normal users. A user 2039 property is not searchable/viewable by normal users. A user
1829 who can search roles can identify users with the admin 2040 who can search roles can identify users with the admin
1830 role. So it does not respond the same as a rest/data/users 2041 role. So it does not respond the same as a rest/data/users
1831 search by a non-admin user. 2042 search by a non-admin user.
1832 ''' 2043 '''
1833 # get real user id 2044 # get real user id
1834 realuid=self.db.getuid() 2045 realuid=self.db.getuid()
1835 2046
1836 def allowed_field(fs): 2047 def allowed_field(fs):
1837 if fs.name in ['username', 'realname' ]: 2048 if fs.name in ['username', 'realname' ]:
1838 # only allow search matches for these fields 2049 # only allow search matches for these fields
1839 return True 2050 return True
1840 elif fs.name in [ '@fields' ]: 2051 elif fs.name in [ '@fields' ]:
1841 for prop in fs.value.split(','): 2052 for prop in fs.value.split(','):
1842 # if any property is unviewable to user, remove 2053 # if any property is unviewable to user, remove
1843 # @field entry. If they can't see it for the admin 2054 # @field entry. If they can't see it for the admin
1844 # user, don't let them see it for any user. 2055 # user, don't let them see it for any user.
1845 if not self.db.security.hasPermission( 2056 if not self.db.security.hasPermission(
1846 'View', realuid, 'user', property=prop, 2057 'View', realuid, 'user', property=prop,
1847 itemid='1'): 2058 itemid='1'):
1848 return False 2059 return False
1849 return True 2060 return True
1850 elif fs.name.startswith("@"): 2061 elif fs.name.startswith("@"):
1851 # allow @page..., @verbose etc. 2062 # allow @page..., @verbose etc.
1852 return True 2063 return True
1853 2064
1854 # deny all other url parmeters 2065 # deny all other url parmeters
1855 return False 2066 return False
1856 2067
1857 # Cleanup input.list to prevent user from probing roles 2068 # Cleanup input.list to prevent user from probing roles
1858 # or viewing things the user should not be able to view. 2069 # or viewing things the user should not be able to view.
1859 input.list[:] = [ fs for fs in input.list 2070 input.list[:] = [ fs for fs in input.list
1860 if allowed_field(fs) ] 2071 if allowed_field(fs) ]
1861 2072
1862 # Add the role filter required to implement the permission 2073 # Add the role filter required to implement the permission
1863 # search 2074 # search
1864 input.list.append(MiniFieldStorage("roles", "Developer")) 2075 input.list.append(MiniFieldStorage("roles", "Developer"))
1865 2076
1866 # change user to acquire permission to search roles 2077 # change user to acquire permission to search roles
1867 self.db.setCurrentUser('admin') 2078 self.db.setCurrentUser('admin')
1868 2079
1869 # Once we have cleaned up the request, pass it to 2080 # Once we have cleaned up the request, pass it to
1870 # get_collection as though /rest/data/users?... has been called 2081 # get_collection as though /rest/data/users?... has been called
1871 # to get @verbose and other args supported. 2082 # to get @verbose and other args supported.
1872 return self.get_collection('user', input) 2083 return self.get_collection('user', input)
1873 2084
1874 Calling this with:: 2085 Calling this with::
1875 2086
1876 curl 'http://example.com/demo/rest/data/@permission/Developer?@fields=realname&roles=Users&@verbose=2' 2087 curl 'http://example.com/demo/rest/data/@permission/Developer?@fields=realname&roles=Users&@verbose=2'
1877 2088
1878 produces output similar to:: 2089 produces output similar to::
1879 2090
1880 { 2091 {
1881 "data": { 2092 "data": {
1882 "collection": [ 2093 "collection": [
1883 { 2094 {
1884 "username": "agent", 2095 "username": "agent",
1885 "link": http://example.com/demo/rest/data/user/4", 2096 "link": http://example.com/demo/rest/data/user/4",
1886 "realname": "James Bond", 2097 "realname": "James Bond",
1887 "id": "4" 2098 "id": "4"
1888 } 2099 }
1889 ], 2100 ],
1890 "@total_size": 1 2101 "@total_size": 1
1891 } 2102 }
1892 } 2103 }
1893 2104
1894 assuming user 4 is the only user with the Developer role. Note that 2105 assuming user 4 is the only user with the Developer role. Note that
1895 the url passes the ``roles=User`` filter option which is silently 2106 the url passes the ``roles=User`` filter option which is silently
1896 ignored. 2107 ignored.
1927 access to an issues' ``times`` property. 2138 access to an issues' ``times`` property.
1928 3. add support for issuing (and validating) JWTs to the rest interface. 2139 3. add support for issuing (and validating) JWTs to the rest interface.
1929 This uses the `Adding new rest endpoints`_ mechanism. 2140 This uses the `Adding new rest endpoints`_ mechanism.
1930 4. configure roundup's config.ini [web] jwt_secret with at least 32 2141 4. configure roundup's config.ini [web] jwt_secret with at least 32
1931 random characters of data. (You will get a message 2142 random characters of data. (You will get a message
1932 ``Support for jwt disabled by admin.`` if it's not long enough.) 2143 ``Support for jwt disabled by admin.`` if it's not long
2144 enough.) If you have openssl installed, you can use the output
2145 of ``openssl rand -base64 32``.
1933 5. add an auditor to make sure that users with this role are appending 2146 5. add an auditor to make sure that users with this role are appending
1934 timelog links to the ``times`` property of the issue. 2147 timelog links to the ``times`` property of the issue.
1935 2148
1936 Create role 2149 Create role
1937 ~~~~~~~~~~~ 2150 ~~~~~~~~~~~
1938 2151
1939 Adding this snippet of code to the tracker's ``schema.py`` should create a role with the 2152 Adding this snippet of code to the tracker's ``schema.py`` should
1940 proper authorization:: 2153 create a role with the proper authorization::
1941 2154
1942 db.security.addRole(name="User:timelog", 2155 db.security.addRole(name="User:timelog",
1943 description="allow a user to create and append timelogs") 2156 description="allow a user to create and append timelogs")
1944 2157
1945 db.security.addPermissionToRole('User:timelog', 'Rest Access') 2158 db.security.addPermissionToRole('User:timelog', 'Rest Access')
1975 View permission), append the newly created timelog id to the (array) 2188 View permission), append the newly created timelog id to the (array)
1976 value, and replace the ``times`` value. 2189 value, and replace the ``times`` value.
1977 2190
1978 Note that the json returned after the operation will include the new 2191 Note that the json returned after the operation will include the new
1979 value of the ``times`` value so your code can verify that it worked. 2192 value of the ``times`` value so your code can verify that it worked.
1980 This does potentially leak info about the previous id's in the field. 2193 This leaks info about the previous id's in the field.
1981 2194
1982 Create rest endpoints 2195 Create rest endpoints
1983 ~~~~~~~~~~~~~~~~~~~~~ 2196 ~~~~~~~~~~~~~~~~~~~~~
1984 2197
1985 Here is code to add to your tracker's ``interfaces.py`` (note code has 2198 Here is code to add to your tracker's ``interfaces.py`` (note code has
1991 @Routing.route("/jwt/issue", 'POST') 2204 @Routing.route("/jwt/issue", 'POST')
1992 @_data_decorator 2205 @_data_decorator
1993 def generate_jwt(self, input): 2206 def generate_jwt(self, input):
1994 """Create a JSON Web Token (jwt) 2207 """Create a JSON Web Token (jwt)
1995 """ 2208 """
2209 import datetime
1996 import jwt 2210 import jwt
1997 import datetime 2211 from roundup.anypy.datetime_ import utcnow
1998 from roundup.anypy.strings import b2s 2212 from roundup.anypy.strings import b2s
1999 2213
2000 # require basic auth to generate a token 2214 # require basic auth to generate a token
2001 # At some point we can support a refresh token. 2215 # At some point we can support a refresh token.
2002 # maybe a jwt with the "refresh": True claim generated 2216 # maybe a jwt with the "refresh": True claim generated
2030 user_roles = list(self.db.user.get_roles(self.db.getuid())) 2244 user_roles = list(self.db.user.get_roles(self.db.getuid()))
2031 2245
2032 claim= { 'sub': self.db.getuid(), 2246 claim= { 'sub': self.db.getuid(),
2033 'iss': self.db.config.TRACKER_WEB, 2247 'iss': self.db.config.TRACKER_WEB,
2034 'aud': self.db.config.TRACKER_WEB, 2248 'aud': self.db.config.TRACKER_WEB,
2035 'iat': datetime.datetime.utcnow(), 2249 'iat': utcnow(),
2036 } 2250 }
2037 2251
2038 lifetime = 0 2252 lifetime = 0
2039 if 'lifetime' in input: 2253 if 'lifetime' in input:
2040 if input['lifetime'].value != 'unlimited': 2254 if input['lifetime'].value != 'unlimited':
2045 " lifetime in seconds. Got %s."%input['lifetime'].value) 2259 " lifetime in seconds. Got %s."%input['lifetime'].value)
2046 else: 2260 else:
2047 lifetime = datetime.timedelta(seconds=86400) # 1 day by default 2261 lifetime = datetime.timedelta(seconds=86400) # 1 day by default
2048 2262
2049 if lifetime: # if lifetime = 0 make unlimited by omitting exp claim 2263 if lifetime: # if lifetime = 0 make unlimited by omitting exp claim
2050 claim['exp'] = datetime.datetime.utcnow() + lifetime 2264 claim['exp'] = utcnow() + lifetime
2051 2265
2052 newroles = [] 2266 newroles = []
2053 if 'roles' in input: 2267 if 'roles' in input:
2054 for role in [ r.lower() for r in input['roles'].value ]: 2268 for role in [ r.lower() for r in input['roles'].value ]:
2055 if role not in rolenames: 2269 if role not in rolenames:
2065 raise UsageError("Role %s is not permitted."%role) 2279 raise UsageError("Role %s is not permitted."%role)
2066 2280
2067 claim['roles'] = newroles 2281 claim['roles'] = newroles
2068 else: 2282 else:
2069 claim['roles'] = user_roles 2283 claim['roles'] = user_roles
2070 secret = self.db.config.WEB_JWT_SECRET 2284
2285 # Sign with newest/first secret.
2286 secret = self.db.config.WEB_JWT_SECRET[0]
2071 myjwt = jwt.encode(claim, secret, algorithm='HS256') 2287 myjwt = jwt.encode(claim, secret, algorithm='HS256')
2072 2288
2073 # if jwt.__version__ >= 2.0.0 jwt.encode() returns string 2289 # if jwt.__version__ >= 2.0.0 jwt.encode() returns string
2074 # not byte. So do not use b2s() with newer versions of pyjwt. 2290 # not byte. So do not use b2s() with newer versions of pyjwt.
2075 result = {"jwt": b2s(myjwt), 2291 result = {"jwt": b2s(myjwt),
2076 } 2292 }
2077 2293
2078 return 200, result 2294 return 200, result
2079 2295
2084 if not 'jwt' in input: 2300 if not 'jwt' in input:
2085 raise UsageError("jwt key must be specified") 2301 raise UsageError("jwt key must be specified")
2086 2302
2087 myjwt = input['jwt'].value 2303 myjwt = input['jwt'].value
2088 2304
2089 secret = self.db.config.WEB_JWT_SECRET 2305 secret = self.db.config.WEB_JWT_SECRET[0]
2306
2307 # only return decoded result if the newest signing key
2308 # is used. Have older keys report an invalid signature.
2090 try: 2309 try:
2091 result = jwt.decode(myjwt, secret, 2310 result = jwt.decode(myjwt, secret,
2092 algorithms=['HS256'], 2311 algorithms=['HS256'],
2093 audience=self.db.config.TRACKER_WEB, 2312 audience=self.db.config.TRACKER_WEB,
2094 issuer=self.db.config.TRACKER_WEB, 2313 issuer=self.db.config.TRACKER_WEB,
2137 curl -s -H "Referer: https://.../demo/" \ 2356 curl -s -H "Referer: https://.../demo/" \
2138 -H "X-requested-with: rest" \ 2357 -H "X-requested-with: rest" \
2139 https://.../demo/rest/JWT/validate?JWT=eyJ0eXAiOiJK...XxMDb-Q3oCnMpyhxPXMAk 2358 https://.../demo/rest/JWT/validate?JWT=eyJ0eXAiOiJK...XxMDb-Q3oCnMpyhxPXMAk
2140 2359
2141 (note no login is required) which returns:: 2360 (note no login is required) which returns::
2142 2361
2143 { 2362 {
2144 "data": { 2363 "data": {
2145 "user": "3", 2364 "user": "3",
2146 "roles": [ 2365 "roles": [
2147 "user:timelog" 2366 "user:timelog"
2149 "iss": "https://.../demo/", 2368 "iss": "https://.../demo/",
2150 "aud": "https://.../demo/", 2369 "aud": "https://.../demo/",
2151 "iat": 1569542404, 2370 "iat": 1569542404,
2152 "exp": 1569546004 2371 "exp": 1569546004
2153 } 2372 }
2154 } 2373 }
2155 2374
2156 2375
2157 There is an issue for `thoughts on JWT credentials`_ that you can view 2376 There is an issue for `thoughts on JWT credentials`_ that you can view
2158 for ideas or add your own. 2377 for ideas or add your own.
2159 2378
2163 ~~~~~~~~~~~ 2382 ~~~~~~~~~~~
2164 2383
2165 See the `upgrading directions`_ on how to use the ``updateconfig`` 2384 See the `upgrading directions`_ on how to use the ``updateconfig``
2166 command to generate an updated copy of config.ini using 2385 command to generate an updated copy of config.ini using
2167 roundup-admin. Then set the ``JWT_secret`` to at least 32 characters 2386 roundup-admin. Then set the ``JWT_secret`` to at least 32 characters
2168 (more is better up to 512 bits). 2387 (more is better up to 512 bits). The output of
2388 ``openssl rand -base64 32`` will fulfill the minimum requirements.
2169 2389
2170 Writing an auditor that uses "db.user.get_roles" to see if the user 2390 Writing an auditor that uses "db.user.get_roles" to see if the user
2171 making the change has the ``user:timelog`` role, and then comparing 2391 making the change has the ``user:timelog`` role, and then comparing
2172 the original ``times`` list to the new list to verify that it is being 2392 the original ``times`` list to the new list to verify that it is being
2173 added to and not changed otherwise is left as an exercise for the 2393 added to and not changed otherwise is left as an exercise for the
2206 2426
2207 uid = self.db.getuid() 2427 uid = self.db.getuid()
2208 class_obj = self.db.getclass('user') 2428 class_obj = self.db.getclass('user')
2209 node = class_obj.getnode(uid) 2429 node = class_obj.getnode(uid)
2210 2430
2211 # set value to 0 to use WEB_API_CALLS_PER_INTERVAL 2431 # set value to 0 to use WEB_API_CALLS_PER_INTERVAL
2212 user_calls = node.__getattr__('rate_limit_calls') 2432 user_calls = node.__getattr__('rate_limit_calls')
2213 # set to 0 to use WEB_API_INTERVAL_IN_SEC 2433 # set to 0 to use WEB_API_INTERVAL_IN_SEC
2214 user_interval = node.__getattr__('rate_limit_interval') 2434 user_interval = node.__getattr__('rate_limit_interval')
2215 2435
2216 return RateLimit(user_calls or calls, 2436 return RateLimit(user_calls or calls,
2217 timedelta(seconds=(user_interval or interval))) 2437 timedelta(seconds=(user_interval or interval)))
2218 else: 2438 else:
2219 # disable rate limiting if either parameter is 0 2439 # disable rate limiting if either parameter is 0
2220 return None 2440 return None
2221 2441
2222 RestfulInstance.getRateLimit = grl 2442 RestfulInstance.getRateLimit = grl
2234 seq 1 300 | xargs -P 20 -n 1 curl --head -u user:password -si \ 2454 seq 1 300 | xargs -P 20 -n 1 curl --head -u user:password -si \
2235 https://.../rest/data/status/new | grep Remaining 2455 https://.../rest/data/status/new | grep Remaining
2236 2456
2237 will show you the number of remaining requests to the REST interface 2457 will show you the number of remaining requests to the REST interface
2238 for the user identified by password. 2458 for the user identified by password.
2459
2460
2461 Notes V2 API
2462 ~~~~~~~~~~~~
2463
2464 These may never be implemented but, some nits to consider.
2465
2466 The shape of a GET and PUT/PATCH responses are different. "attributes"
2467 is used for GET and "attribute" is used with PATCH/PUT. A PATCH or a
2468 PUT can update multiple properties when used with an item endpoint.
2469 "attribute" kind of makes sense when used with a property endpoint
2470 but.... Maybe standardize on one shape so the client doesn't have to
2471 special case?

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