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