
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "Evergreen ILS". The branch, main has been updated via 491816b999f6732db8fd11f235d97bca31ffe426 (commit) via 5fdc3d0b3c466c71a8956aab613f84eb50d4bbfe (commit) via 6ef607d3c225c0dc238240828d89eb5346ea2a08 (commit) via 649f38ac6900fdb6936b0356db846bd72d0a9246 (commit) via f2bfeb5778692b1a563d6c777c83049bb5bd5cba (commit) via 862ad66d1d6e62e649abb3fcbf67d87def0b679a (commit) via c8e65252507ff94dff5e1e19835c356f92321c2b (commit) via 2a308edb8312a2b5ea422892de5fd7e8e0a2771a (commit) via 5c79bf7d69d99f0e72882d46b112d51d3162d539 (commit) via 461e5cda890ebaab4d013f8e0b3a14e50e09b6e7 (commit) from b10b089b32f7f920764177942965bd10af9b9c27 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 491816b999f6732db8fd11f235d97bca31ffe426 Author: Jeff Godin <jgodin@tadl.org> Date: Fri Mar 21 23:57:03 2025 -0400 LP#2067414: Adjust docs wording based on feedback Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/docs/modules/integrations/pages/restful_api.adoc b/docs/modules/integrations/pages/restful_api.adoc index 7626f5652a..000da07a0c 100644 --- a/docs/modules/integrations/pages/restful_api.adoc +++ b/docs/modules/integrations/pages/restful_api.adoc @@ -5,7 +5,7 @@ The below documentation describes the new RESTful API suite for Evergreen. -For now, this is an experimental feature, intended as a starting point to encourage further testing. A more fully developed version of the API will be included in a future release. +This is an initial release of the foundation and infrastructure to support early adoption, providing a starting point to encourage further testing and integration. Additional API functionality will be included in a future release. == Terminology == commit 5fdc3d0b3c466c71a8956aab613f84eb50d4bbfe Author: Jeff Godin <jgodin@tadl.org> Date: Fri Mar 21 23:56:36 2025 -0400 LP#2067414: Stamp upgrade script diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index 5a26813b54..13d695aadf 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps BEFORE INSERT OR UPDATE ON config.db_patch_dependencies FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates'); -INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1467', :eg_version); -- sandbergja/blake/sleary +INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1468', :eg_version); -- miker/rdavis/jeffdavis/jeff CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql b/Open-ILS/src/sql/Pg/upgrade/1468.schema.openapi.sql similarity index 99% rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql rename to Open-ILS/src/sql/Pg/upgrade/1468.schema.openapi.sql index 07afff5149..6b7e60ee76 100755 --- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql +++ b/Open-ILS/src/sql/Pg/upgrade/1468.schema.openapi.sql @@ -1,5 +1,7 @@ BEGIN; +SELECT evergreen.upgrade_deps_block_check('1468', :eg_version); + -- Necessary pre-seed data commit 6ef607d3c225c0dc238240828d89eb5346ea2a08 Author: Jeff Davis <jdavis@sitka.bclibraries.ca> Date: Wed Mar 19 15:49:12 2025 -0700 LP#2067414: Describe REST API as "experimental" in documentation Signed-off-by: Jeff Davis <jdavis@sitka.bclibraries.ca> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/docs/modules/integrations/pages/restful_api.adoc b/docs/modules/integrations/pages/restful_api.adoc index b5a6ea9b9f..7626f5652a 100644 --- a/docs/modules/integrations/pages/restful_api.adoc +++ b/docs/modules/integrations/pages/restful_api.adoc @@ -3,7 +3,9 @@ == Introduction == -The below documentation describes the new RESTful API suite for Evergreen, rewritten in late 2024. +The below documentation describes the new RESTful API suite for Evergreen. + +For now, this is an experimental feature, intended as a starting point to encourage further testing. A more fully developed version of the API will be included in a future release. == Terminology == commit 649f38ac6900fdb6936b0356db846bd72d0a9246 Author: Galen Charlton <gmc@equinoxOLI.org> Date: Mon Mar 17 17:51:20 2025 -0400 LP#2067414: adjust and document API version This patch documents expectations around the versioning of the OpenAPI endpoints. Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Davis <jdavis@sitka.bclibraries.ca> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/src/support-scripts/openapi_server b/Open-ILS/src/support-scripts/openapi_server index a7e2d0567e..a8fbb625aa 100755 --- a/Open-ILS/src/support-scripts/openapi_server +++ b/Open-ILS/src/support-scripts/openapi_server @@ -85,7 +85,7 @@ my $config = { } }, servers => [ - { url => "/openapi3/v1" } + { url => "/openapi3/v0" } ], tags => [], components => { diff --git a/docs/modules/integrations/pages/restful_api.adoc b/docs/modules/integrations/pages/restful_api.adoc index 5e5308a57a..b5a6ea9b9f 100644 --- a/docs/modules/integrations/pages/restful_api.adoc +++ b/docs/modules/integrations/pages/restful_api.adoc @@ -639,7 +639,7 @@ The Evergreen OpenAPI endpoints are not meant for use by humans directly in a br The way OpenAPI clients and servers work together in Evergreen, through this development specifically, is as follows: -. The server software produces an API specification document in JSON or YAML. This is available at https://<hostname>/openapi3/v1 on any fully installed instance of this development. +. The server software produces an API specification document in JSON or YAML. This is available at https://<hostname>/openapi3/v0 on any fully installed instance of this development. . The client software consumes that specification document in order to understand what API calls are available, how it should send parameter data to the API calls, and what the format of the output of an API call will look like. There are standard tools to automate much of the client-side work, but the result of creating a functioning OpenAPI client will be an application making HTTP requests. @@ -674,6 +674,10 @@ NOTE: The JSON Schema is very large, and tends to cause the both locally hosted The web-based Swagger-UI editor and visualization tools are not robust or sophisticated enough to handle such a large and complex component schema. This is a limitation of the basic demo Swagger tools. True client applications do not try to render the full JSON Schema, are written to be robust and correct, and are not expected to have these sorts of issues if they are designed well. +=== API Versioning === + +Evergreen's OpenAPI support establishes a framework for clients to access and manipulate Evergreen data as well as a set of predefined endpoints. Consequently, additional endpoints are expected to be added over time. Like any API, the definitions of endpoints are subject to change, especially as the number of clients using the endpoints grows. An API version stamp in the path component of the base URL will be used to signal whether breaking changes have been introduced in the API. The initial release of the API sets the version as `v0` (i.e., `https://<hostname>/openapi3/v0`). This will get incremented whenever backwards-incompatible changes get made to existing endpoint or if changes to core Fieldmapper IDL classes could break clients. + [[adding_endpoints_example]] == Example: Adding endpoints to Evergreen's OpenAPI server == commit f2bfeb5778692b1a563d6c777c83049bb5bd5cba Author: Mike Rylander <mrylander@gmail.com> Date: Tue Mar 11 17:37:58 2025 -0400 Docs: RESTful API documentation Co-authored-by: Andrea Buntz Neiman <abneiman@equinoxinitiative.org> Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Andrea Buntz Neiman <abneiman@equinoxinitiative.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/docs/modules/integrations/assets/images/restful_api/api_explorer.png b/docs/modules/integrations/assets/images/restful_api/api_explorer.png new file mode 100644 index 0000000000..3fca2bcb2b Binary files /dev/null and b/docs/modules/integrations/assets/images/restful_api/api_explorer.png differ diff --git a/docs/modules/integrations/assets/images/restful_api/block_expanded.png b/docs/modules/integrations/assets/images/restful_api/block_expanded.png new file mode 100644 index 0000000000..1f00178298 Binary files /dev/null and b/docs/modules/integrations/assets/images/restful_api/block_expanded.png differ diff --git a/docs/modules/integrations/assets/images/restful_api/create_endpoint.png b/docs/modules/integrations/assets/images/restful_api/create_endpoint.png new file mode 100644 index 0000000000..ea8322ad15 Binary files /dev/null and b/docs/modules/integrations/assets/images/restful_api/create_endpoint.png differ diff --git a/docs/modules/integrations/assets/images/restful_api/expected_parameters.png b/docs/modules/integrations/assets/images/restful_api/expected_parameters.png new file mode 100644 index 0000000000..3c0b77cce3 Binary files /dev/null and b/docs/modules/integrations/assets/images/restful_api/expected_parameters.png differ diff --git a/docs/modules/integrations/assets/images/restful_api/method_details.png b/docs/modules/integrations/assets/images/restful_api/method_details.png new file mode 100644 index 0000000000..a9f4fe5667 Binary files /dev/null and b/docs/modules/integrations/assets/images/restful_api/method_details.png differ diff --git a/docs/modules/integrations/nav.adoc b/docs/modules/integrations/nav.adoc index 9b04db7e34..82ef54bdbe 100644 --- a/docs/modules/integrations/nav.adoc +++ b/docs/modules/integrations/nav.adoc @@ -1,5 +1,6 @@ * xref:integrations:introduction.adoc[Integrating Evergreen with Other Tools] ** xref:integrations:ezproxy.adoc[EZProxy] ** xref:integrations:patron-api.adoc[PatronAPI authentication] +** xref:integrations:restful_api.adoc[RESTful API] ** xref:integrations:web_services.adoc[Web Services] diff --git a/docs/modules/integrations/pages/restful_api.adoc b/docs/modules/integrations/pages/restful_api.adoc new file mode 100644 index 0000000000..5e5308a57a --- /dev/null +++ b/docs/modules/integrations/pages/restful_api.adoc @@ -0,0 +1,877 @@ += RESTful API in Evergreen = +:toc: + +== Introduction == + +The below documentation describes the new RESTful API suite for Evergreen, rewritten in late 2024. + +== Terminology == + +**OpenAPI** + +An industry standard https://swagger.io/specification/[API specification] managed by the Linux Foundation. + +**JSON Schema** + +An industry standard https://json-schema.org/specification[specification] for describing data structures shared between systems using JSON documents. + +**Operation ID** + +The unique identifier of an OpenAPI method served via a specific HTTP method and path. + +**Endpoint** + +The term used within the Evergreen OpenAPI Server to describe the functional response handler assigned to an OpenAPI Operation ID. + +**Endpoint Parameter** + +The description and specification of an OpenAPI input parameter that can be mapped to an internal method endpoint parameter. + +**Endpoint Response** + +The description and specification of a response structure that is returned from an endpoint. + +**Endpoint Set** + +A group of endpoints that can be configured with access using a single permission set. Analogous to the tag concept in OpenAPI. + +**Permission Set** + +A group of permissions that, when all are applied to an Evergreen user that has been configured as an API Integrator, allow access to an endpoint or endpoint set. + +**Rate Limit Definition** + +A combination of time interval and access attempt count that define the maximum average rate of access attempts allowed by a specific API Integrator, or from an IP address or address range, or globally for the Evergreen OpenAPI Server instance. + +**API Integrator** + +An Evergreen user that has been configured specifically for use in accessing the OpenAPI endpoints provided by the Evergreen OpenAPI Server. + +== Architecture == + +=== Integrators === + +Access is permitted to the OpenAPI server for those Evergreen user accounts that have been promoted to an Integrator account, and have had an additional password with the new api password type associated with them. These accounts do not need to have a known main password, nor do they need to have login type permissions other than API_LOGIN. Such accounts are effectively API-only accounts, and while they may be given some set of staff-like permissions in order to access necessary patron and transaction data, they do not need to have staff client or other elevated access to Evergreen. + +Today, promotion of an account to be an Integrator is managed using the `api_ctl` tool. The `api_ctl` command path to add a user as an Integrator is *integrator -> add -> USERNAME* and the command path to set the password for Integrator API login is *integrator -> load -> USERNAME -> password*. This can be accomplished in a single command directly from the command line: + +`$ api_ctl --command integrator add USERNAME password` + +which will then prompt for the API password to assign to the newly promoted user. A newly added Integrator is enabled by default. However, an Integrator promotion can be disable, re-enabled, or removed similarly: + +`$ api_ctl --command integrator disable USERNAME` + +`$ api_ctl --command integrator enable USERNAME` + +`$ api_ctl --command integrator remove USERNAME` + +More details on the integrator control functionality of the `api_ctl` tool can be found below. + +While it is possible to use any valid authentication token to request methods through the OpenAPI server, including those created through OPAC and Staff Client login, logging in through the OpenAPI server itself using the `/self/auth` path is additionally protected and the user must meet the following minimum requirements: + +* The user must have the API_LOGIN permission +* The user must be promoted as an Integrator +* That Integrator promotion must be enabled +* The Integrator must have an 'api'-type password + +These restrictions are imposed by the open-ils.auth OpenSRF application, and are intended to prevent accidentally allowing OpenAPI access. A caveat to the foregoing is noted below, to facilitate convenient patron access to the OpenAPI server. + +=== Rate Limit Definitions === + +Rate of access to each endpoint can be restricted by the use of Rate limit definitions. These simple rules consist of a limit interval and a limit count, where there must be no more than limit count requests over the preceding limit interval time. + +Each endpoint request is evaluated in the context of the user making the request and their IP address, the Rate limit definition attached to the endpoint or the default fallback limit, any Rate limit definitions attached to the endpoint set into which the endpoint is placed, and any Rate limit definitions that combine the user and IP address with the endpoint or relevant endpoint set. + +If any user or IP address related Rate limit definitions are configured for the request, the most restrictive of those is applied before allowing access. If no user or IP address related Rate limit definitions are configured, the most restrictive of the general endpoint and endpoint Set limits is used. + +=== Permission Sets === + +Access to each API method is restricted based on the Evergreen permissions assigned to the user that has been promoted to Integrator. For flexibility and ease of configuration, these access permissions can be grouped into permission sets. These permission sets are then assigned to endpoints and endpoint set, described below, and the +Integrator must have all of the permissions in at least one of the permission sets in effect for an endpoint in order to successfully call the API method. + +For example, there are two stock permission sets associated with the _self_ endpoint set, which includes the primary authentication API method located at the `/self/auth` path and methods that allow user self-service functionality. + +One set, the _Self - standard permissions_ permission set, includes just the OPAC_LOGIN permission. This allows +normal patron-type users that have been promoted to Integrator and have been given an API password to log into the OpenAPI server using Basic authentication. This user will log in with that separate password to access the self-service methods, or use the URL parameter login to supply a login type of "opac" with their main username and password credentials. Another set, the _Self - API only_ permission set, contains the new API_LOGIN and REST.api permissions and allows login and self-service requests only for Integrator users that have both of those new permission. They do not need to have the OPAC_LOGIN permission. + +As a more complicated example, there exists a stock API method at the path `/patron/:userid` which returns detailed user information on the user with an ID specified at the `:userid` placeholder. This method is in the patron-oriented _patron_ endpoint set which is protected by default using two permission sets, one containing the API_LOGIN, REST.api, and REST.api.patrons permissions, and the other containing the STAFF_LOGIN and VIEW_USER permissions. Additionally, an administrator may associate the endpoint with an endpoint-specific permission set, named _Custom patron retrieval_ for this example, containing REST.api and REST.api.patrons.detail.read permissions. + +Thus, there are three permission sets in effect for the `/patron/:userid` endpoint: + +* _Patrons - standard permissions_: STAFF_LOGIN, VIEW_USER +* _Patrons - API only_: API_LOGIN, REST.api, REST.api.patrons +* _Custom patron retrieval_: API_LOGIN, REST.api, REST.api.patrons.detail.read + +The first permission set would be congruent with a normal Evergreen staff account accessing the OpenAPI server with a Staff Client authentication token, the second with an Integrator account that has been granted general patron-related API access, and the third with an Integrator account that has been granted specific access to that one API endpoint by having the REST.api.patrons.detail.read permission, but does not have general patron-related REST.api.patrons permission. + +NOTE: For convenience, it is possible to pass a login type parameter either using the URL parameter style login, or by adding the desired login type to the end of the Basic authentication string, separated from the username and password by a colon, before base64 encoding the data. This allows non-Integrator accounts to log into the OpenAPI server if they could otherwise log into Evergreen using a login type other than "api". + +If an authenticated account accessing this API has all of the permissions in any of those sets, then the API's logic is allowed to execute. Users are assigned permissions in the normal Evergreen way, by having the permission added to their account directly or by being made a member of a primary or secondary permission group which has the permission assigned to it. + +Once the permission to execute the API's logic has been established, the requesting account's normal permissions must still be applied to any underlying action. In this example, if the backend method that retrieves patron record detail requires the VIEW_USER permission be granted to the requestor at the home library of the patron record, that must still hold in order for the API to return the requested data. + +==== Default Permission Groups ==== + +The new, additional permission groups meant to support API access for Integrator accounts (that is, via the "api-only" permission sets) look like this: + +[width="100%",cols="10%,46%,44%",options="header",] +|=== +|*id* |*perm_group* |*permissions* +|25 |API Integrator |API_LOGIN ; REST.api +|26 |Patron API |REST.api.patrons +|27 |Organizational Unit API |REST.api.orgs +|28 |Bib Record API |REST.api.bibs +|29 |Item Record API |REST.api.items +|30 |Holds API |REST.api.holds +|31 |Debt Collection API |REST.api.collections +|32 |Course Reserves API |REST.api.courses +|=== + +=== Endpoints and Endpoint Sets === + +Evergreen's OpenAPI Server exposes specific business logic functions and data access pathways as API endpoints. + +Each of these endpoints present a single backend function, usually a way to request particular data or initiate a specific action, by translating OpenAPI paths, HTTP methods, and HTTP headers and parameters to handler functions and their necessary parameters. + +These handler functions may be either OpenSRF service and method pairs, or when some amount of pre- or post-processing of an OpenSRF method call is required, a Perl module and optional sub name that supplies any necessary additional logic. + +Once the handler has run, the requested output is translated to an OpenAPI response, normally as JSON or XML formatted data. The requests may make use of parameter data passed via HTTP path components or query parameters, HTTP cookies or headers, or the HTTP request body. + +Endpoints can be thought of as OpenAPI wrappers to existing Evergreen functionality implemented in OpenSRF. + +Structurally, each endpoint consists of the following information: + +* _Operation ID_ - The unique identifier for the endpoint. +* _Path_ - The HTTP path, which may include contextual information such as placeholders for identifiers or behavioral options, that makes up part of the URL used to address the service through a standard web server. +* _HTTP Method_ - The HTTP verb used to signal the type of action being requested; one of `get`, `put`, `post`, `delete`, and `patch`. +* _Security method_ - How the OpenAPI server should expect to receive authentication and authorization credentials. +** The specialized authentication endpoint can use Basic and Parameter Security. +** All other endpoints default to Bearer Authentication, though all of Bearer, Cookie, and Parameter are generally usable for most endpoints. +* _Summary_ - A textual description of the purpose of the endpoint. +* _Method Source_ - Either an OpenSRF application name, or Perl package name. +* _Method Name_ - Either an OpenSRF method from the OpenSRF application, or Perl subroutine from the Perl package. +* _Method Parameters_ - A space-separated mapping from the available named OpenAPI parameters and environmental data to the positional parameters of the backend OpenSRF method or Perl subroutine. +* _Active flag_ - Controls general access to the configured endpoint; when active, the endpoint is exposed through OpenAPI. +* _Default Rate Limit Definition_ - Rate limit definition to apply to the endpoint, unless a context-specific user rate limit or IP address rate limit is configured. +** The authentication endpoints use the "100 requests per second" rate limit by default. +** All other endpoints default to "1 request per second per user" if the user is known to the server at request time, or to "1 request per second per IP address" if the IP address is available to the server at request time, or "1 request per second globally" if neither are available at the time of the request. + +These endpoints can then be gathered together into logical groups, called endpoint sets, to simplify access configuration and documentation. Endpoint sets can supply permission sets and rate limit definitions for endpoints that they contain. For permission sets, if an endpoint is in more than one endpoint set, all mapped permission sets are applied to the endpoint and successful authorization against any of the permission sets will allow access. + +Rate limit definition selection is more complicated, but fully predictable. + +First, all definitions related to the relevant endpoint sets (and the specific endpoint) that are attached with the context of the user or their IP address, if any, are compared and the strictest rate limit definition is applied. If there are no user or IP address contextual rate limit definitions in place, the strictest rate limit definition attached to the endpoint or any of its endpoint set is applied to the request, first against the user for the endpoint if the user is known, then against the IP address for the endpoint if the IP address is known, or finally, globally against the endpoint. + +==== Endpoint Parameters ==== + +Because OpenSRF and OpenAPI have different calling conventions as well as different response format and structure, it is necessary to map between these two programmatic interfaces. Where possible, the OpenAPI server translates between simple JSON Objects and Evergreen fieldmapper objects automatically, validating both input and output for validity and correctness. + +Parameter mapping is configured by describing the expected incoming parameter layout in terms of input type, location, and name. Parameters to OpenAPI endpoints can be passed as part of the URL path, as URL query parameters, as HTTP headers, or as cookies. + +The datatype of each parameter is specified as either a JSON schema datatype, with the ability to specify an expected element type for arrays or expect semantic format for scalar types, or as an Evergreen fieldmapper class hint. + +**Supported JSON schema datatypes** + +* object +* array +* boolean +* string +* integer +* number + +**Supported optional JSON schema scalar semantic format descriptors** + +* String formats +** date-time +** date +** time +** interval +** email +** uri +** identifier +** password +* Number formats +** money +** float +* Integer format +** int64 + +Additionally, all Fieldmapper class hints can be used as parameter types, and the OpenAPI server will validate incoming JSON Objects to confirm that they contain all required properties, and do not contain any that are not defined by the Fieldmapper IDL. + +In addition to the location, type, and format configuration, OpenAPI parameters can be marked as required, which will be validated by the OpenAPI server, and can define a default value to be passed when non-required parameters are not supplied by the user. + +Once parameters have been described, they are available for mapping into OpenSRF positional parameters. This OpenSRF calling convention mapping is configured at the endpoint level. The named OpenAPI parameters are accessible in two ways: + +* Via the `param` namespace, which supplies the last instance of the parameter processed when the parameter is expected to carry one value and a scalar should be passed to the backend method. For example `param.userid` might be the userid value extracted from a URL path placeholder. +* Via the `every_param` namespace, if a parameter is expected to be repeatable and all values should be passed to the backend method. For example `every_param.org` might be all values from a repeatable `org` URL query parameter. + +In addition to the named parameters provided directly to the OpenAPI server as described above and accessed through the `param` and `every_param` namespaces, several built-in reserved parameter names are available to all non-authentication methods: + +* `eg_auth_token` - The authentication token provided with the HTTP request, usually via the authentication header as a Bearer token. +* `eg_user_id` - The user id of the authenticated user. +* `eg_user_obj` - The full user object of the authenticated user. +* `req.json` - The HTTP request body, parsed as JSON and provided as a Perl data object to the backend method. +* `req.text` - The HTTP request body, as plain text. + +Finally, quoted string literals can be used as mapped positional parameters in cases where the backend method requires some value, but there is no need to make the parameter available from end-user input. + +Configured OpenAPI parameters are mapped to backend parameters in a space separated list associated with the endpoint, which also defines the type, manner, and location of the backend method. For example, the "/self/me" path that retrieves the user record for the logged in user, and the "/patron/\{userid}" path that attempts to retrieve an arbitrary user record, subject to all API access and Evergreen permission restrictions, use the same backend method, and substantively differ only in their parameter mapping: + +* /self/me: `eg_auth_token eg_user_id` +* /patron/\{userid}: `eg_auth_token param.userid` + +==== Endpoint Responses ==== + +Endpoint responses are described in a very similar way to parameters, with optional output validation and optional Fieldmapper class hint or JSON Schema type information. However, they differ in that responses cannot have a default value, and are also described by their HTTP response status code (200, 401, 404, 500, etc) and message, as well as their expected MIME content type. The content type will generally be either `application/json or text/plain`, though, for example, the provided bibliographic record retrieval endpoint also offers both `application/xml` and `application/octet-stream` to support alternate negotiated formats. + +In order to support varying response formats, the OpenAPI server investigates the HTTP Accept header for each request and determines the best response type-match to the requested content type; this is called the resolved content format. The resolution of the best content type to respond with based on the Accept header is performed per the HTTP specification, taking into account the "q" weight parameter as well as the completeness (or, use of wildcards) for the types. This is made available to the parameter mapping function with the special reserved name `eg_req_resolved_content_format`, and when using a Perl module and method rather than an OpenSRF service and method, to the handler method via the `stash()` function of the invocant, which is passed as its first method parameter. + +The default resolved content format is assumed to be `json` to support JSON Schema and Fieldmapper object output which will be the most common result. The high-level types that can be detected and used are: + +* json +* text +* html +* xml +* binary + +For those responses that return JSON data, either validated or unvalidated, OpenAPI server supports an `Accept` header extension, provided using the HTTP-standard `Accept` header extension mechanism, which allows an API consumer to request slim responses stripped of all properties that contain a `null` value. As a peer to the normal "q" weight parameter in the `Accept` header, a "filterNulls" parameter with a true value ("t" or "1") will enable this mode. + +This "filterNulls" behavioral modifier can also be supplied via a new HTTP header called `X-EG-OpenAPI-Options`. + +Adding this option through either header can reduce the size of most responses that contain Fieldmapper objects by more than 50%. + +==== Default Endpoint Set ==== + +The default endpoint set (with endpoints listed for convenience) to permission set mapping is: + +[width="100%",cols="18%,37%,45%",options="header",] +|=== +|*endpoint_set* |*endpoints* |*perm_sets* +|orgs a| +retrieveFullOrgTree + +retrieveOneOrg + +retrieveOrgList + +retrievePartialOrgTree + +a| +"Orgs - API only" (API_LOGIN, REST.api, REST.api.orgs) + +"Orgs - standard permissions" (OPAC_LOGIN) + +|collections a| +collectionsPatronsOfInterest + +collectionsPatronsOfInterestWarning + +a| +"Collections - API only" (API_LOGIN, REST.api, REST.api.collections) + +"Collections - standard permissions" (STAFF_LOGIN, VIEW_USER) + +|holds a| +retrieveHold + +retrieveHoldPickupLocations + +a| +"Holds - API only" (API_LOGIN, REST.api, REST.api.holds) + +"Holds - standard permissions" (STAFF_LOGIN, VIEW_USER) + +|self a| +authenticateUser + +cancelSelfHold + +checkinSelfCirc + +logoutUser + +renewSelfCirc + +requestSelfCirc + +requestSelfHold + +retrieveSelfCirc + +retrieveSelfCircHistory + +retrieveSelfCircs + +retrieveSelfHold + +retrieveSelfHolds + +retrieveSelfProfile + +retrieveSelfXact + +retrieveSelfXacts + +selfActivePenalties + +selfPenalty + +selfUpdateParts + +updateSelfHold + +a| +"Self - API only" (API_LOGIN, REST.api) + +"Self - standard permissions" (OPAC_LOGIN) + +|courses a| +activeCourses + +activeRoles + +retrieveCourse + +retrieveCourseMaterials + +retrieveCourseUsers + +a| +"Courses - API only" (API_LOGIN, REST.api) + +"Courses - standard permissions" (STAFF_LOGIN) + +|patrons a| +cancelPatronHold + +checkinPatronCirc + +collectionsGetPatronDetail + +collectionsPutPatronInCollections + +collectionsRemovePatronFromCollections + +patronATEvent + +patronATEvents + +patronActiveMessages + +patronActivePenalties + +patronActivityLog + +patronMessage + +patronMessageArchive + +patronMessageUpdate + +patronPenalty + +renewPatronCirc + +requestPatronCirc + +requestPatronHold + +retrievePatronCirc + +retrievePatronCircHistory + +retrievePatronCircs + +retrievePatronHold + +retrievePatronHolds + +retrievePatronProfile + +retrievePatronXact + +retrievePatronXacts + +searchPatrons + +updatePatronHold + +verifyUserCredentials + +a| +"Patrons - API only" (API_LOGIN, REST.api, REST.api.patrons) + +"Patrons - standard permissions" (STAFF_LOGIN, VIEW_USER) + +|items a| +createItem + +deleteItem + +newItems + +retrieveItem + +updateItem + +a| +"Items - API only" (API_LOGIN, REST.api, REST.api.items) + +"Items - standard permissions" (OPAC_LOGIN) + +|bibs a| +bibDisplayFields + +createOneBib + +deleteOneBib + +retrieveOneBib + +retrieveOneBibHoldings + +updateBREParts + +updateOneBib + +a| +"Bibs - API only" (API_LOGIN, REST.api, REST.api.bibs) + +"Bibs - standard permissions" (OPAC_LOGIN) + +|=== + +== Output Filtering == + +Available properties on Fieldmapper object output can be restricted through the use of several Library and User settings. These settings define whitelisted and blacklisted properties, with blacklisted property removal taking precedence over whitelisted property inclusion, where different settings conflict. + +=== API-global property filtering configuration === + +The content of these settings is a comma-separated list of values, which are made up of the Fieldmapper class hint and the specific Fieldmapper property to be addressed. For example, to change the visibility of the +Date of Birth column from the `actor.usr table`, one would add `au.dob` to the appropriate setting value, where "au" is the Fieldmapper class hint for the class representing the actor.usr table in the database, and "dob" is the Fieldmapper property representing, and column name, which holds the date of birth data. + +Whitelisting one or more properties on a Fieldmapper class will cause all other properties to be redacted by setting their values to `null`. Blacklisting one or more properties will cause only those named properties to be redacted in this way. + +Library settings are evaluated in the context of the Integrator user's home library, and are inheritable from parent locations in the same manner as all other Library settings. Two Library settings are for +API-global use: + +* REST.api.whitelist_properties - Allow a particular set of properties, and only those properties, to be delivered to the Integrator across all API endpoints. +* REST.api.blacklist_properties - Disallow a particular set of properties to be delivered to the Integrator across all API endpoints. + +User settings can also be used to add Integrator-specific whitelist and blacklist rules. To apply API-global, Integrator-specific properties restrictions, two new stock User Setting Types are available, with the same name and function as the Library settings above. Because these are user-specific, administrators can provide specific Integrator accounts access to more data with more whitelisted properties, or further restrict access by adding additional blacklist properties. + +=== Endpoint specific property restrictions === + +In addition to the API-global Library and User settings, administrators can create new Organizational Unit Setting Types (AKA Library Setting Types) and User Setting Types through the Server Administration interface. Setting types must be named following a specific pattern. The setting types must start with the string `REST.api.whitelist_properties.` or `REST.api.blacklist_properties.` for whitelisting or blacklisting respectively -- note the period -- and end with the endpoint's Operation ID. For example, if an administrator would like to restrict the output of the `searchPatron` endpoint so that it only returns the idof the patron, and the client is expected to retrieve the full patron record separately, they could configure following: + +* Create a new Organizational Unit Setting Type with the name: `REST.api.whitelist_properties.searchPatron` +* Set the value of the new Library Setting to `au.id` at the top of the Organizational hierarchy in the Library Settings Editor. + +User Setting Types can be created through the Server Administration interface, as well. However, because there is no Staff Client interface for general User Setting maintenance, the `api_ctl` tool, discussed below, must be used to configure Integrator-specific whitelist and blacklist rules. Integrator-specific setting types are created automatically by `api_ctl` if they do not already exist for a particular endpoint, so administrators are not required to create User Setting Types by hand when using the `api_ctl` tool. + +=== Applicability and scope === + +Both whitelist and blacklist settings can be used at the same time, though the one primary case for this would be to add a property to a more specific blacklist when it is already present on a general whitelist. This may be useful in the case where general API access should allow retrieval of a particular piece of information, but a specific integration should not provide this information because it is likely to be visible through some third party interface if it is delivered to the client application. + +This output property restriction mechanism depends upon Evergreen's knowledge of the Integrator account making the request. In practice this means that all endpoints with the exception of the primary authentication endpoint, which only delivers an authentication token, will be protected using this feature. However, if local, custom endpoints are configured for an Evergreen instance that do not use the built-in security mechanisms, they cannot make use of this property restriction feature. + +== OpenAPI server configuration == + +As mentioned above regarding the promotion of normal Evergreen user accounts to API Integrators, a new command line tool, called `api_ctl`, is available to configure and control the OpenAPI backend setup. + +This tool presents a text menu-driven interface for administrators to configure the backend. In many situations, the tool can also be used with direct command line parameters when the administrator knows the menu path they would normally take to effect the desired configuration change. + +A step-by-step example of this process is given below in the section xref:integrations:restful_api.adoc#adding_endpoints_example[Example: Adding endpoints to Evergreen’s OpenAPI server]. + +Once the desired configuration changes have been made, all instances of the API server must be restarted in order to load the new configuration. + +=== Menu hierarchy === + +There are several standard options available at most levels of the menu hierarchy: + +* top - go to the top level of the command hierarchy +* back - go "up" one level in the command hierarchy +* show - display the details of the currently loaded context configuration objects +* details - toggle whether additional information about the objects configured by the current hierarchical command set is shown with the show command +* quit - leave the tool + +There are common commands available for many objects that can be controlled through the api_ctl interface. Generally, objects can be listed, added, edited, and removed with menu options that are: + +* list - Show a summary list of the objects of the type controlled by the current level of the menu hierarchy. +* load - Set the current context object to be configured, based on the current level of the menu hierarchy. +* unload - Unset the current context object the type of which is based on the current level of the menu hierarchy. +* add - Add a new object of the type controlled by the current level of the menu hierarchy. +* edit - Edit the current context object controlled by the current level of the menu hierarchy. +* remove - Remove an object controlled by the current level of the menu hierarchy. + +You can see all options available at the current level of the option hierarchy by pressing the tab key. + +The layout of the menu hierarchy when started without command line options is as follows: + +* api - Control API endpoint configuration +** endpoints - Configure endpoints +*** list - List all endpoints +*** load - Set the current context endpoint +*** unload - Unset the current context endpoint +*** add +*** edit +*** remove +*** activate - Activate an inactive endpoint +*** deactivate - Deactivate an active endpoint +*** parameters - Configure the Parameters of the context endpoint +**** list - List the OpenAPI Parameters of the context endpoint +**** add +**** edit +**** remove +*** responses - Configure the Response structure of the context endpoint +**** list - List all Responses configured for the context endpoint +**** add +**** edit +**** remove +*** sets - Add and remove endpoint Set mappings for the context endpoint +**** list - List the endpoint set to which the endpoint is assigned +**** add - Add the context endpoint to an endpoint Set +**** remove - Remove the context endpoint from an endpoint Set +*** perm_sets - Manage permission sets assigned to the context endpoint +**** list - List all permission sets assigned to the context endpoint +**** add - Add a permission set to allow access to the context endpoint +**** remove - Remove an assigned permission set from the context endpoint +*** rate_limits - Used to manage the endpoint-specific rate-limiting configuration for the context endpoint +**** ip_address - Manage IP address-based rate limiting +***** list +***** add +***** edit +***** remove +**** integrator - Manage Integrator-based rate limiting +***** list +***** add +***** edit +***** remove +** sets - Configure endpoint sets +*** list - List all endpoint sets +*** load - Set the current context endpoint set +*** unload - Unset the current context endpoint set +*** add +*** edit +*** remove +*** activate - Activate an inactive endpoint set +*** deactivate - Deactivate an active endpoint set +*** endpoints - Manage endpoints mapped into the current context endpoint set +**** list - List all endpoints in the current context endpoint set +**** add - Add an endpoint to the current context endpoint set +**** remove - Remove an endpoints from the current context endpoint set +*** perm_sets - Manage permission sets assigned to the context endpoint set +**** list - List all permission sets assigned to the context endpoint set +**** add - Add a permission set to allow access to the context endpoint set +**** remove - Remove an assigned permission set from the context endpoint set +*** rate_limits Used to manage the endpoint set-wide rate-limiting configuration for the context endpoint set +**** ip_address - Manage IP address-based rate limiting for the endpoint set +***** list +***** add +***** edit +***** remove +**** integrator - Manage Integrator-based rate limiting for the endpoint set +***** list +***** add +***** edit +***** remove +* integrator - Manage Integrator-promoted Evergreen user accounts +** list - List all integrators +** load - Set the current context Integrator +** unload - Unset the current context Integrator +** add - Promote an Evergreen user account to an API Integrator account +** remove - Remove the Integrator promotion from an Evergreen user account +** enable - Enable a disabled Integrator for API login +** disable - Disable an enabled Integrator for API login +** password - Set the API-specific password for the context integrator +** permissions - Manage the user-specific permissions granted to an Integrator +*** list - List the permissions granted to the current context Integrator +*** add - Add a permission to the Integrator +*** remove - Remove a permission from the Integrator +** groups - Manage the Secondary Permission Groups to which an Integrator +belongs +*** list - List the current context Integrator's Secondary Permission Groups +*** add - Add the Integrator to a Secondary Permission Group +*** remove - Remove the Integrator from a Secondary Permission Group +** global_property_whitelist - Manage the Integrator-specific, API-global Fieldmapper property whitelist for an Integrator +*** list - Show the current Integrator-specific, API-global whitelist +*** set - Edit the current Integrator-specific, API-global whitelist +*** remove - Remove the current Integrator-specific, API-global whitelist +** global_property_blacklist - Manage the Integrator-specific, API-global Fieldmapper property blacklist for an Integrator +*** list - Show the current Integrator-specific, API-global blacklist +*** set - Edit the current Integrator-specific, API-global blacklist +*** remove - Remove the current Integrator-specific, API-global blacklist +** endpoint_property_whitelist - Manage the Integrator-specific, endpoint-specific Fieldmapper property whitelists for an Integrator +*** list - List the current endpoint-specific whitelist +*** add - Add one endpoint-specific whitelist +*** edit - Edit one endpoint-specific whitelist +*** remove - Remove one endpoint-specific whitelist +** endpoint_property_blacklist - Manage the Integrator-specific, +endpoint-specific Fieldmapper property blacklists for an Integrator +*** list - List all current endpoint-specific blacklist +*** add - Add one endpoint-specific blacklist +*** edit - Edit one endpoint-specific blacklist +*** remove - Remove one endpoint-specific blacklist +* control +** rate_limit - Manage rate limit definitions +*** list - List all rate limit definitions +*** load - Set the current context rate limit definition +*** unload - Unset the current context rate limit definition +*** add +*** edit +*** remove +** perm_sets - Manage permission sets +*** list - List all permission sets +*** load - Set the current context permission set +*** unload - Unset the current context permission set +*** add +*** edit +*** remove +*** permissions - Manage the permissions attached to the current context +permission set +**** list - List all permissions attached to the current context permission set +**** add - Add a permission to the current context permission set +**** remove - Remove a permission from the current context permission set + +== Testing and Using the API == + +The Evergreen OpenAPI endpoints are not meant for use by humans directly in a browser URL bar, though it does use the same underlying protocols and mechanisms as a user-oriented interface. Instead, it is meant for programmatic access using standard OpenAPI calling semantics, as described by https://swagger.io/docs/specification/v3_0/about/[https://swagger.io/docs/specification/v3_0/about/]. + +The way OpenAPI clients and servers work together in Evergreen, through this development specifically, is as follows: + +. The server software produces an API specification document in JSON or YAML. This is available at https://<hostname>/openapi3/v1 on any fully installed instance of this development. +. The client software consumes that specification document in order to understand what API calls are available, how it should send parameter data to the API calls, and what the format of the output of an API call will look like. + +There are standard tools to automate much of the client-side work, but the result of creating a functioning OpenAPI client will be an application making HTTP requests. + +There are many OpenAPI library generators available for use by developers for most common client programming languages. Among them are: + +* For general use: +** https://openapi-generator.tech/[OpenAPI Generator] +** https://swagger.io/tools/swagger-codegen/[API Code & Client Generator | Swagger Codegen] +** OpenAPIs.org https://tools.openapis.org/categories/sdk.html[SDK] generator listing +* Perl client libraries +** https://metacpan.org/pod/OpenAPI::Client[OpenAPI::Client - A client for talking to an Open API powered server - metacpan.org] + +=== Authentication and authorization === + +Obtaining an Evergreen auth token is accomplished by sending an HTTP *GET* request to the `/self/auth` path with the appropriate credentials. + +The credential parameter names are described in the API specification document. They can be passed using either + +. A standard HTTP Basic authorization header (with an optional third component for the login type); or +. Through query parameters with the names `u` for username, `p` for password, and `t` for login type. + +Using the standard HTTP content negotiation `Accept` header, the client can ask for the token to be delivered as either plain text, which is useful for tasks like direct testing and high-level shell scripting, or as JSON data, which is the default and is usually better for use by actual client applications. + +The output of this request, which is a standard Evergreen auth token used by all authenticated client code in Evergreen, is then used in a standard HTTP Bearer authentication header to identify the session in later requests. For ease of testing and some added flexibility, the auth token may also be passed in the URL as a query parameter called `ses`, or in either of the modern Evergreen-standard cookies called `eg.auth.token` and `ses`, or the new, Evergreen OpenAPI-specific cookie called `eg.api.token`. + +=== Other testing tools === + +Administrators can install Swagger-UI visualization tools so that developers can see the list of endpoints. These tools make use of the API specification document to assist with authorization and help the user authenticate with the API. + +NOTE: The JSON Schema is very large, and tends to cause the both locally hosted demonstration Swagger-UI visualization tools and the free editor hosted at editor.swagger.io to hang. This development intentionally creates a run-time translation mapping between Evergreen's Fieldmapper data structures that describe Evergreen data layout and standard JSON Schema object definitions, and there are many hundreds of object types that reference each other. + +The web-based Swagger-UI editor and visualization tools are not robust or sophisticated enough to handle such a large and complex component schema. This is a limitation of the basic demo Swagger tools. True client applications do not try to render the full JSON Schema, are written to be robust and correct, and are not expected to have these sorts of issues if they are designed well. + +[[adding_endpoints_example]] +== Example: Adding endpoints to Evergreen's OpenAPI server == + +This demonstration makes use of the https://github.com/EquinoxOpenLibraryInitiative/evg-api-explorer[Evergreen API Explorer], a Vue-based OpenSRF API exploration tool created at Equinox as a replacement for the original docgen.xsl OpenSRF API documentation publishing mechanism. + +=== User Id by barcode or username === + +Using the Evergreen API Explorer, we can see that the open-ils.actor application provides several methods that may suit our purpose here. + +image::restful_api/api_explorer.png[Evergreen API Explorer] + +The parameter documentation is not great for any of these, but we can see where to find the underlying code by expanding the block. + +image::restful_api/block_expanded.png[Expand the block to view details] + +The implementation for `open-ils.actor.user.retrieve_id_by_barcode_or_username` is in the OpenILS::Application::Actor Perl module, in the sub named `retrieve_usr_id_via_barcode_or_usrname`. Using this information, we can see that the parameters expected by the OpenSRF method are an authentication token, an optional barcode, and an optional username. + +image::restful_api/expected_parameters.png[Example of expected parameters] + +We also see that the method will return either an numeric user id, or an ILS Event object upon error or permission restriction. + +image::restful_api/method_details.png[Example of method details] + +Using this information, we can immediately provide RESTful OpenAPI endpoints to return a user id by either barcode or username simply by +wrapping the OpenSRF method directly. We can either insert the necessary +endpoint configuration directly into the database, or use the api_ctl +tool to configure the new endpoints. + +.Direct SQL configuration +[source, postgresql] +---- +BEGIN; + +INSERT INTO openapi.endpoint ( + operation_id, path, http_method, + summary, + method_source, method_name, + method_params +) VALUES ( + 'patronIdByCardBarcode', '/patrons/by_barcode/:barcode/id', 'get', + 'Retrieve patron id by barcode', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', +'eg_auth_token param.barcode' +); + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES +('patronIdByCardBarcode','barcode','path','string',TRUE); + +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES +('patronIdByCardBarcode','integer'); + +INSERT INTO openapi.endpoint ( + operation_id, path, http_method, + summary, + method_source, method_name, + method_params +) VALUES ( + 'patronIdByUsername', '/patrons/by_username/:username/id', 'get', + 'Retrieve patron id by username', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', +'eg_auth_token "" param.username' +); + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES +('patronIdByUsername','username','path','string',TRUE); + +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES +('patronIdByUsername','integer'); + +INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) + SELECT operation_id, 'patrons' FROM openapi.endpoint WHERE operation_id like 'patronIdBy%'; + +COMMIT; +---- + +.api_ctl configuration +[source, console] +---- +$ api_ctl -- api endpoints add + # ... supply information about patronIdByCardBarcode endpoint, as above +API Endpoint actions: parameters +API Endpoint Parameter actions: add + # ... supply information about patronIdByCardBarcode path parameter +API Endpoint Parameter actions: back +API Endpoint actions: responses + # ... supply information about patronIdByCardBarcode success response +API Endpoint Responses actions: back +API Endpoint actions: sets +API Endpoint Assigned Sets actions: add +API Set: patrons +API Endpoint Assigned Sets actions: back +API Endpoint actions: add + # ... supply information about patronIdByUsername endpoint +API Endpoint actions: parameters +API Endpoint Parameter actions: add + # ... supply information about patronIdByUsername path parameter +API Endpoint Parameter actions: back +API Endpoint actions: responses + # ... supply information about patronIdByUsername success response +API Endpoint Responses actions: back +API Endpoint actions: sets +API Endpoint Assigned Sets actions: add +API Set: patrons +API Endpoint Assigned Sets actions: quit +---- + +=== User Object by barcode or username === + +Likewise, we can use just a small amount of additional code to create an endpoint to return the full user object in the same format as is returned by the `/patron/:id` endpoint path. As this new method will be a stock endpoint, we will add it to the built-in OpenILS::OpenAPI::Controller::patron Perl module, but it can live anywhere that the Perl interpreter can find modules. + +image:restful_api/create_endpoint.png[Creating an endpoint] + +The OpenILS::OpenAPI::Controller module namespace contains many endpoint examples and helper methods that are useful for the creation of OpenAPI endpoints. + +The parameters passed to the handler functions are exactly those that are defined for the OpenAPI endpoint, via its parameter list, following an invocant passed in the first parameter position. The effective invocant is the active Mojolicious Controller object. Extensive https://docs.mojolicious.org/Mojolicious[[documentation] on the https://docs.mojolicious.org/Mojolicious/Controller[Mojolicious Controller] is available with the Mojolicious Perl module. + +And, as before, we can then register the new endpoints with the OpenAPI server either using direct SQL or the api_ctl tool. + +.Direct SQL configuration +[source, postgresql] +---- +BEGIN; + +INSERT INTO openapi.endpoint ( + operation_id, path, http_method, + summary, + method_source, method_name, + method_params +) VALUES ( + 'patronByCardBarcode', '/patrons/by_barcode/:barcode', 'get', + 'Retrieve patron id by barcode', + 'OpenILS::OpenAPI::Controller::patron','user_by_identifier_string', + 'eg_auth_token param.barcode' +); + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES +('patronByCardBarcode','barcode','path','string',TRUE); + +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES +('patronByCardBarcode','au'); + +INSERT INTO openapi.endpoint ( + operation_id, path, http_method, + summary, + method_source, method_name, + method_params +) VALUES ( + 'patronByUsername, '/patrons/by_username/:username', 'get', + 'Retrieve patron id by username', + 'OpenILS::OpenAPI::Controller::patron','user_by_identifier_string', + 'eg_auth_token "" param.username' +); + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES +('patronByUsername','username','path','string',TRUE); + +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES +('patronByUsername','au'); + +INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) + SELECT operation_id, 'patrons' FROM openapi.endpoint WHERE operation_id like 'patronBy%'; + +COMMIT; +---- + +.api_ctl configuration +[source, console] +---- +$ api_ctl -- api endpoints add + # ... supply information about patronByCardBarcode endpoint, as above +API Endpoint actions: parameters +API Endpoint Parameter actions: add + # ... supply information about patronByCardBarcode path parameter +API Endpoint Parameter actions: back +API Endpoint actions: responses + # ... supply information about patronByCardBarcode success response +API Endpoint Responses actions: back +API Endpoint actions: sets +API Endpoint Assigned Sets actions: add +API Set: patrons +API Endpoint Assigned Sets actions: back +API Endpoint actions: add + # ... supply information about patronByUsername endpoint +API Endpoint actions: parameters +API Endpoint Parameter actions: add + # ... supply information about patronByUsername path parameter +API Endpoint Parameter actions: back +API Endpoint actions: responses + # ... supply information about patronByUsername success response +API Endpoint Responses actions: back +API Endpoint actions: sets +API Endpoint Assigned Sets actions: add +API Set: patrons +API Endpoint Assigned Sets actions: quit +---- + +=== Enabling the new endpoints === + +The OpenAPI server must be restarted once any new endpoint configuration is applied. This allows the OpenAPI server to read the new endpoint configuration and add the appropriate routes and handlers. commit 862ad66d1d6e62e649abb3fcbf67d87def0b679a Author: Mike Rylander <mrylander@gmail.com> Date: Tue Dec 10 12:53:00 2024 -0500 LP#2067414: OpenAPI Infrastructure This commit implements an OpenAPI interface against which external clients can be developed. Components consist of: * Database and IDL data structures used to configure and manage the OpenAPI implementation. * A command line configuration tool for managing the OpenAPI endpoints, API Integrator user accounts, permission and data visibility restrictions, and rate limiting settings. * A stand-alone OpenSRF client script implementing an OpenAPI server using configuration data stored in the Evergreen database. * Seed data creating a set of OpenAPI endpoints useful for interacting with: - the Evergreen authentication infrastructure - your own account-related data - other patrons' records - transactional records (holds, circs) - item records - bibliographic records - course reserves Release-note: Implements an OpenAPI server for Evergreen Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 1a46e9a133..b8e9d0ef68 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -4197,7 +4197,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA </links> <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> <actions> - <retrieve permission="VIEW_USER user_request.view" context_field="home_ou"/> + <retrieve permission="VIEW_USER user_request.view" context_field="home_ou" owning_user="id"/> </actions> </permacrud> </class> @@ -4871,7 +4871,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> <actions> <create permission="UPDATE_USER"><context link="usr" field="home_ou"/></create> - <retrieve permission="VIEW_USER"><context link="usr" field="home_ou"/></retrieve> + <retrieve permission="VIEW_USER" owning_user="usr"><context link="usr" field="home_ou"/></retrieve> <update permission="UPDATE_USER"><context link="usr" field="home_ou"/></update> <delete permission="UPDATE_USER"><context link="usr" field="home_ou"/></delete> </actions> @@ -7527,7 +7527,7 @@ SELECT usr, </links> <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> <actions> - <retrieve permission="VIEW_USER user_request.view"> + <retrieve permission="VIEW_USER user_request.view" owning_user="usr"> <context link="usr" field="home_ou"/> </retrieve> </actions> @@ -14400,6 +14400,468 @@ SELECT usr, </permacrud> </class> + <!-- OpenAPI class defs --> + <class id="ojsd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::json_schema_datatype" oils_persist:tablename="openapi.json_schema_datatype" reporter:label="OpenAPI JSON Schema Datatype"> + <fields oils_persist:primary="name"> + <field name="name" reporter:label="Name" reporter:datatype="id"/> + <field name="label" reporter:label="Label" reporter:datatype="text" oils_obj:required="true"/> + <field name="description" reporter:label="Description" reporter:datatype="text"/> + </fields> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="ojsf" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::json_schema_format" oils_persist:tablename="openapi.json_schema_format" reporter:label="OpenAPI JSON Schema Data Format"> + <fields oils_persist:primary="name"> + <field name="name" reporter:label="Name" reporter:datatype="id"/> + <field name="label" reporter:label="Label" reporter:datatype="text" oils_obj:required="true"/> + <field name="description" reporter:label="Description" reporter:datatype="text"/> + </fields> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oint" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::integrator" oils_persist:tablename="openapi.integrator" reporter:label="OpenAPI Integrator"> + <fields oils_persist:primary="id"> + <field name="id" reporter:label="User/Integrator ID" reporter:datatype="id"/> + <field name="enabled" reporter:label="Enabled?" reporter:datatype="bool" oils_obj:required="true"/> + </fields> + <links> + <link field="id" reltype="has_a" key="id" map="" class="au"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="orld" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::rate_limit_definition" oils_persist:tablename="openapi.rate_limit_definition" reporter:label="OpenAPI Endpoints" oils_persist:restrict_primary="100"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.rate_limit_definition_id_seq"> + <field name="id" reporter:label="ID" reporter:datatype="id" reporter:selector="name"/> + <field name="name" reporter:label="Name" reporter:datatype="text" oils_obj:required="true"/> + <field name="limit_interval" reporter:label="Interval" reporter:datatype="interval" oils_obj:required="true"/> + <field name="limit_count" reporter:label="Count" reporter:datatype="int" oils_obj:required="true"/> + </fields> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="orlem" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_user_rate_limit_map" oils_persist:tablename="openapi.endpoint_user_rate_limit_map" reporter:label="User OpenAPI Endpoint Rate Limit Map"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_user_rate_limit_map_id_seq"> + <field name="id" reporter:label="User Endpoint Rate Limit Map ID" reporter:datatype="id"/> + <field name="accessor" reporter:label="API Accessor" reporter:datatype="text"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="rate_limit" reporter:label="Rate Limit" reporter:datatype="link"/> + </fields> + <links> + <link field="accessor" reltype="has_a" key="name" map="" class="au"/> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="rate_limit" reltype="has_a" key="name" map="" class="orld"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="orlesm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set_user_rate_limit_map" oils_persist:tablename="openapi.endpoint_set_user_rate_limit_map" reporter:label="User OpenAPI Endpoint Set Rate Limit Map"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_user_rate_limit_map_id_seq"> + <field name="id" reporter:label="User Endpoint Set Rate Limit Map ID" reporter:datatype="id"/> + <field name="accessor" reporter:label="API Accessor" reporter:datatype="text"/> + <field name="endpoint_set" reporter:label="Endpoint Set" reporter:datatype="link"/> + <field name="rate_limit" reporter:label="Rate Limit" reporter:datatype="link"/> + </fields> + <links> + <link field="accessor" reltype="has_a" key="name" map="" class="au"/> + <link field="endpoint_set" reltype="has_a" key="name" map="" class="oes"/> + <link field="rate_limit" reltype="has_a" key="name" map="" class="orld"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="orleipm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_ip_rate_limit_map" oils_persist:tablename="openapi.endpoint_ip_rate_limit_map" reporter:label="IP OpenAPI Endpoint Rate Limit Map"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_ip_rate_limit_map_id_seq"> + <field name="id" reporter:label="IP Endpoint Rate Limit Map ID" reporter:datatype="id"/> + <field name="ip_range" reporter:label="IP with CIDR Mask" reporter:datatype="text"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="rate_limit" reporter:label="Rate Limit" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="rate_limit" reltype="has_a" key="name" map="" class="orld"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="orlesipm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set_ip_rate_limit_map" oils_persist:tablename="openapi.endpoint_set_ip_rate_limit_map" reporter:label="IP OpenAPI Endpoint Set Rate Limit Map"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_ip_rate_limit_map_id_seq"> + <field name="id" reporter:label="IP Endpoint Set Rate Limit Map ID" reporter:datatype="id"/> + <field name="ip_range" reporter:label="IP with CIDR Mask" reporter:datatype="text"/> + <field name="endpoint_set" reporter:label="Endpoint Set" reporter:datatype="link"/> + <field name="rate_limit" reporter:label="Rate Limit" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint_set" reltype="has_a" key="name" map="" class="oes"/> + <link field="rate_limit" reltype="has_a" key="name" map="" class="orld"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve permission="ADMIN_OPENAPI" global_required="true"/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oep" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint" oils_persist:tablename="openapi.endpoint" reporter:label="OpenAPI Endpoints"> + <fields oils_persist:primary="operation_id"> + <field name="operation_id" reporter:label="Operation ID" reporter:datatype="id"/> + <field name="path" reporter:label="Route Path" reporter:datatype="text" oils_obj:required="true"/> + <field name="http_method" reporter:label="HTTP Method" reporter:datatype="text" oils_obj:required="true"/> + <field name="summary" reporter:label="Summary" reporter:datatype="text" oils_obj:required="true"/> + <field name="security" reporter:label="Security Scheme" reporter:datatype="text" oils_obj:required="true"/> + <field name="rate_limit" reporter:label="Default Rate Limit" reporter:datatype="link"/> + <field name="method_source" reporter:label="Method Source (Perl Module or OpenSRF service)" reporter:datatype="text" oils_obj:required="true"/> + <field name="method_name" reporter:label="Method Name (Perl or OpenSRF method)" reporter:datatype="text" oils_obj:required="true"/> + <field name="method_params" reporter:label="Method Parameters (comma-separated list)" reporter:datatype="text"/> + <field name="active" reporter:label="Active" reporter:datatype="bool" oils_obj:required="true"/> + <field name="parameters" reporter:label="Route Parameters" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="responses" reporter:label="Route Responses" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="perms" reporter:label="Direct Permissions" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="perm_sets" reporter:label="Direct Permission Sets" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="endpoint_sets" reporter:label="Endpoint Sets" reporter:datatype="link" oils_persist:virtual="true"/> + </fields> + <links> + <link field="rate_limit" reltype="has_a" key="id" map="" class="orld"/> + <link field="parameters" reltype="has_many" key="endpoint" map="" class="oepparam"/> + <link field="responses" reltype="has_many" key="endpoint" map="" class="oepres"/> + <link field="perms" reltype="has_many" key="endpoint" map="perm" class="oep_perm_maps"/> + <link field="perm_sets" reltype="has_many" key="endpoint" map="perm_set" class="oep_perm_set_maps"/> + <link field="endpoint_sets" reltype="has_many" key="endpoint" map="endpoint_set" class="oes_maps"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oepparam" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_param" oils_persist:tablename="openapi.endpoint_param" reporter:label="OpenAPI Endpoint Parameter Schemas"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_param_id_seq"> + <field name="id" reporter:label="Param Schema ID" reporter:datatype="id"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link" oils_obj:required="true"/> + <field name="name" reporter:label="Name" reporter:datatype="text" oils_obj:required="true"/> + <field name="required" reporter:label="Required" reporter:datatype="bool" oils_obj:required="true"/> + <field name="in_part" reporter:label="In Part" reporter:datatype="text" oils_obj:required="true"/> + <field name="fm_type" reporter:label="Fieldmapper hint" reporter:datatype="text"/> + <field name="schema_type" reporter:label="JSON Schema Datatype" reporter:datatype="link"/> + <field name="schema_format" reporter:label="JSON Schema Data Format" reporter:datatype="link"/> + <field name="array_items" reporter:label="JSON Schema Array Item Datatypes" reporter:datatype="link"/> + <field name="default_value" reporter:label="Default value" reporter:datatype="text"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="schema_type" reltype="has_a" key="name" map="" class="ojsd"/> + <link field="schema_format" reltype="has_a" key="name" map="" class="ojsf"/> + <link field="array_items" reltype="has_a" key="name" map="" class="ojsd"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oepres" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_response" oils_persist:tablename="openapi.endpoint_response" reporter:label="OpenAPI Endpoint Response Schemas"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_response_id_seq"> + <field name="id" reporter:label="Response Schema ID" reporter:datatype="id"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link" oils_obj:required="true"/> + <field name="validate" reporter:label="Validate" reporter:datatype="bool"/> + <field name="status" reporter:label="Status Code" reporter:datatype="int"/> + <field name="content_type" reporter:label="Content Type" reporter:datatype="text"/> + <field name="description" reporter:label="Status Description" reporter:datatype="text"/> + <field name="fm_type" reporter:label="Fieldmapper hint" reporter:datatype="text"/> + <field name="schema_type" reporter:label="JSON Schema Datatype" reporter:datatype="link"/> + <field name="schema_format" reporter:label="JSON Schema Data Format" reporter:datatype="link"/> + <field name="array_items" reporter:label="JSON Schema Array Item Datatypes" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="schema_type" reltype="has_a" key="name" map="" class="ojsd"/> + <link field="schema_format" reltype="has_a" key="name" map="" class="ojsf"/> + <link field="array_items" reltype="has_a" key="name" map="" class="ojsd"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="ops" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::perm_set" oils_persist:tablename="openapi.perm_set" reporter:label="OpenAPI Permission Sets" oils_persist:restrict_primary="1001"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.perm_set_id_seq"> + <field name="id" reporter:label="Permission Set ID" reporter:datatype="id"/> + <field name="name" reporter:label="Name" reporter:datatype="text"/> + <field name="perms" reporter:label="Permissions" reporter:datatype="link" oils_persist:virtual="true"/> + </fields> + <links> + <link field="perms" reltype="has_many" key="perm_set" map="perm" class="opspm"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="opspm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::perm_set_perm_map" oils_persist:tablename="openapi.perm_set_perm_map" reporter:label="OpenAPI Permission Set Permission Maps"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_perm_map_id_seq"> + <field name="id" reporter:label="Endpoint Set Permission Map ID" reporter:datatype="id"/> + <field name="perm_set" reporter:label="Permission Set" reporter:datatype="link"/> + <field name="perm" reporter:label="Permission" reporter:datatype="link"/> + </fields> + <links> + <link field="perm_set" reltype="has_a" key="id" map="" class="ops"/> + <link field="perm" reltype="has_a" key="id" map="" class="ppl"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oes" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set" oils_persist:tablename="openapi.endpoint_set" reporter:label="OpenAPI Endpoint Set"> + <fields oils_persist:primary="name"> + <field name="name" reporter:label="Set Name" reporter:datatype="id"/> + <field name="description" reporter:label="Set Description" reporter:datatype="text" oils_obj:required="true"/> + <field name="active" reporter:label="Active" reporter:datatype="bool" oils_obj:required="true"/> + <field name="endpoint" reporter:label="Endpoints" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="perm_set_maps" reporter:label="Permission Set Maps" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="perm_sets" reporter:label="Permission Sets" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="perms" reporter:label="Direct Permissions" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="rate_limit" reporter:label="Default Rate Limit" reporter:datatype="link"/> + </fields> + <links> + <link field="perm_set_maps" reltype="has_many" key="endpoint_set" map="" class="oes_perm_set_maps"/> + <link field="perm_sets" reltype="has_many" key="endpoint_set" map="perm_set" class="oes_perm_set_maps"/> + <link field="perms" reltype="has_many" key="endpoint_set" map="perm" class="oes_perm_maps"/> + <link field="endpoints" reltype="has_many" key="endpoint_set" map="endpoint" class="oes_maps"/> + <link field="rate_limit" reltype="has_a" key="id" map="" class="orld"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oes_maps" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set_endpoint_map" oils_persist:tablename="openapi.endpoint_set_endpoint_map" reporter:label="OpenAPI Endpoint Set Map"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_endpoint_map_id_seq"> + <field name="id" reporter:label="Endpoint Permission Map ID" reporter:datatype="id"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="endpoint_set" reporter:label="Endpoint Set" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="endpoint_set" reltype="has_a" key="name" map="" class="oes"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oes_perm_set_maps" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set_perm_set_map" oils_persist:tablename="openapi.endpoint_set_perm_set_map" reporter:label="OpenAPI Endpoint Set Permission Set Maps"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_perm_set_map_id_seq"> + <field name="id" reporter:label="Endpoint Set Permission Set Map ID" reporter:datatype="id"/> + <field name="endpoint_set" reporter:label="Endpoint Set" reporter:datatype="link"/> + <field name="perm_set" reporter:label="Permission Set" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint_set" reltype="has_a" key="name" map="" class="oes"/> + <link field="perm_set" reltype="has_a" key="id" map="" class="ops"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oep_perm_set_maps" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_perm_set_map" oils_persist:tablename="openapi.endpoint_perm_set_map" reporter:label="OpenAPI Endpoint Permission Set Maps"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_perm_set_map_id_seq"> + <field name="id" reporter:label="Endpoint Permission Set Map ID" reporter:datatype="id"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="perm_set" reporter:label="Permission_set" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="name" map="" class="oep"/> + <link field="perm_set" reltype="has_a" key="id" map="" class="ops"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oes_perm_maps" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_set_perm_map" oils_persist:tablename="openapi.endpoint_set_perm_map" reporter:label="OpenAPI Endpoint Set Permission Maps"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_perm_map_id_seq"> + <field name="id" reporter:label="Endpoint Set Permission Map ID" reporter:datatype="id"/> + <field name="endpoint_set" reporter:label="Endpoint Set" reporter:datatype="link"/> + <field name="perm" reporter:label="Permission" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint_set" reltype="has_a" key="name" map="" class="oes"/> + <link field="perm" reltype="has_a" key="name" map="" class="ppl"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + <class id="oep_perm_maps" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="openapi::endpoint_perm_map" oils_persist:tablename="openapi.endpoint_perm_map" reporter:label="OpenAPI Endpoint Permission Maps"> + <fields oils_persist:primary="id" oils_persist:sequence="openapi.endpoint_set_endpoint_map_id_seq"> + <field name="id" reporter:label="Endpoint Set Endpoint Map ID" reporter:datatype="id"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="perm" reporter:label="Permission" reporter:datatype="link"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="perm" reltype="has_a" key="name" map="" class="ppl"/> + </links> + <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1"> + <actions> + <create permission="ADMIN_OPENAPI" global_required="true"/> + <retrieve/> + <update permission="ADMIN_OPENAPI" global_required="true"/> + <delete permission="ADMIN_OPENAPI" global_required="true"/> + </actions> + </permacrud> + </class> + + + <class id="oaal" controller="open-ils.cstore" oils_obj:fieldmapper="openapi::authen_attempt_log" oils_persist:tablename="openapi.authen_attempt_log" reporter:label="OpenAPI Authentication Attempt Log"> + <fields oils_persist:primary="request_id"> + <field name="request_id" reporter:label="Request ID" reporter:datatype="text"/> + <field name="attempt_time" reporter:label="Attempt Time" reporter:datatype="timestamp"/> + <field name="ip_addr" reporter:label="IP Address" reporter:datatype="text"/> + <field name="cred_user" reporter:label="User Credential" reporter:datatype="text"/> + <field name="token" reporter:label="Authentication Token" reporter:datatype="text"/> + <field name="dispatch_log_entry" reporter:label="Dispatch Log Entry" reporter:datatype="link" oils_persist:virtual="true"/> + </fields> + <links> + <link field="dispatch_log_entry" reltype="might_have" key="request_id" map="" class="oedl"/> + </links> + </class> + + <class id="oeaal" controller="open-ils.cstore" oils_obj:fieldmapper="openapi::endpoint_access_attempt_log" oils_persist:tablename="openapi.endpoint_access_attempt_log" reporter:label="OpenAPI Endpoint Access Attempt Log"> + <fields oils_persist:primary="request_id"> + <field name="request_id" reporter:label="Request ID" reporter:datatype="text"/> + <field name="endpoint" reporter:label="Endpoint" reporter:datatype="link"/> + <field name="attempt_time" reporter:label="Request Time" reporter:datatype="timestamp"/> + <field name="ip_addr" reporter:label="IP Address" reporter:datatype="text"/> + <field name="accessor" reporter:label="API Accessor" reporter:datatype="text"/> + <field name="allowed" reporter:label="Access Permitted?" reporter:datatype="bool"/> + <field name="token" reporter:label="Authentication Token" reporter:datatype="text"/> + <field name="dispatch_log_entry" reporter:label="Dispatch Log Entry" reporter:datatype="link" oils_persist:virtual="true"/> + </fields> + <links> + <link field="endpoint" reltype="has_a" key="operation_id" map="" class="oep"/> + <link field="accessor" reltype="has_a" key="name" map="" class="au"/> + <link field="dispatch_log_entry" reltype="might_have" key="request_id" map="" class="oedl"/> + </links> + </class> + + <class id="oedl" controller="open-ils.cstore" oils_obj:fieldmapper="openapi::endpoint_dispatch_log" oils_persist:tablename="openapi.endpoint_dispatch_log" reporter:label="OpenAPI Endpoint Dispatch Log"> + <fields oils_persist:primary="request_id"> + <field name="request_id" reporter:label="Request ID" reporter:datatype="text"/> + <field name="complete_time" reporter:label="Complete Time" reporter:datatype="timestamp"/> + <field name="error" reporter:label="Access Permitted?" reporter:datatype="bool"/> + <field name="authen_log_entry" reporter:label="Authentication Log Entry" reporter:datatype="link" oils_persist:virtual="true"/> + <field name="endpoint_log_entry" reporter:label="Endpoint Access Log Entry" reporter:datatype="link" oils_persist:virtual="true"/> + </fields> + <links> + <link field="authen_log_entry" reltype="might_have" key="request_id" map="" class="oaal"/> + <link field="endpoint_log_entry" reltype="might_have" key="request_id" map="" class="oeaal"/> + </links> + </class> + <!-- Simple Reporter classes are currently used only by the SR UI --> <class id="srcirc" controller="simple-reporter.ui" oils_persist:virtual="true" reporter:label="Simple Reporter Circulation"> <oils_persist:source_definition><![CDATA[ diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller.pm new file mode 100644 index 0000000000..e941e35e1a --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller.pm @@ -0,0 +1,149 @@ +package OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Application::AppUtils; +use MIME::Base64; + +our $VERSION = 1; +my $U = "OpenILS::Application::AppUtils"; + +sub retrieve_one_object_via_pcrud { + my ($controller, $ses, $type, $pkey_value, $flesh_fields, $flesh_depth) = @_; + $flesh_fields ||= {}; + $flesh_depth ||= 1; + + my $method = "retrieve_$type"; + + my $o = new_editor( + personality => 'open-ils.pcrud', + authtoken => $ses + )->$method([ + $pkey_value, + {flesh => $flesh_depth, flesh_fields => $flesh_fields} + ]); + + $controller->res->code(404) unless ($o); + return $o; +} + +sub authenticateUser { + my ($c, $u, $p, $t) = @_; + + my ($type, $creds) = split ' ', $c->req->headers->authorization || ''; + if ($creds and $type =~ /basic/i) { + $creds = decode_base64($creds); + ($u,$p,$t) = split ':', $creds; + } + + $t ||= 'api'; # Default to requiring the API_LOGIN permission and an integrator account + + my $auth = {}; + if ($u) { + my $throttle_user = check_auth_limits($u, $c->forwarded_for); + + if ($throttle_user) { + log_authentication_attempt($c, $u); + $c->res->code(429); + $c->res->headers->header('Retry-After' => $throttle_user); + return $auth; + } elsif ($p) { + $c->app->log->trace("Attempting login for user $u, login type $t"); + $auth = $U->simplereq( + 'open-ils.auth', 'open-ils.auth.login', + { username => $u, + password => $p, + type => $t + } + ); + + if (!$auth or !$auth->{textcode} or $auth->{textcode} ne 'SUCCESS') { + log_authentication_attempt($c, $u); + $c->res->code(401); + $c->res->message('Login failed'); + return $auth; + } else { + log_authentication_attempt($c, $u, $auth->{payload}->{authtoken}); + my $resp_type = $c->stash('eg_req_resolved_content_format') || 'json'; + return $auth->{payload}->{authtoken} if ($resp_type eq 'text'); + return { token => $auth->{payload}->{authtoken} }; + } + + } else { + log_authentication_attempt($c, $u); + $c->res->code(400); + return $auth; + } + } + + log_authentication_attempt($c); + $c->res->code(400); + return $auth; +} + +sub check_auth_limits { + my $username = shift; + my $ip = shift; + + my $limits = new_editor()->json_query({from => ['openapi.check_auth_endpoint_rate_limit', $username, $ip]}); + return $$limits[0]{'openapi.check_auth_endpoint_rate_limit'} if @$limits; + + return undef; # proceed +} + +sub log_authentication_attempt { + my $c = shift; + my $user = shift; + my $token = shift; + + my $authen_attempt_log = Fieldmapper::openapi::authen_attempt_log->new; + $authen_attempt_log->request_id( $c->req->request_id ); + $authen_attempt_log->ip_addr( $c->forwarded_for ); + $authen_attempt_log->cred_user( $user ); + $authen_attempt_log->token( $token ); + + my $e = new_editor(xact=>1); + $e->create_openapi_authen_attempt_log($authen_attempt_log); + $e->commit; +} + +sub where_clause_from_triples { + my ($fields, $ops, $values) = @_; + + my %search_parts; + while (my $f = shift @$fields) { + my $o = shift @$ops; + my $v = shift @$values; + $search_parts{$f} ||= []; + push @{$search_parts{$f}}, { $f => { $o => $v } }; + } + + my %search = ( '-and' => [] ); + for my $f (keys %search_parts) { + my $p = $search_parts{$f}; + if (@$p > 1) { # or them for the same field + push @{$search{'-and'}}, { '-or' => $p }; + } else { + push @{$search{'-and'}}, $$p[0]; + } + } + + return \%search; +} + +sub apply_blob_to_object { + my ($obj,$blob,$allowed) = @_; + + my %defaults; + if (ref($allowed) eq 'HASH') { + %defaults = %$allowed; + $allowed = [ keys %defaults ]; + } + + my %parts = %$blob; # work on a copy + %parts = %parts{@$allowed} + if ($allowed and @$allowed); # trim if requested + + $obj->$_(defined($parts{$_}) ? $parts{$_} : $defaults{$_}) for keys %parts; +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/bib.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/bib.pm new file mode 100644 index 0000000000..25dfc462bd --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/bib.pm @@ -0,0 +1,236 @@ +package OpenILS::OpenAPI::Controller::bib; +use OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Application::AppUtils; +use OpenILS::Application::Cat::AssetCommon; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Const qw/:const/; +use MARC::Record; + +our $VERSION = 1; +our $U = "OpenILS::Application::AppUtils"; + +sub fetch_one_bib { + my ($c, $bib) = @_; + my $bre = new_editor()->retrieve_biblio_record_entry($bib); + my $resp_type = $c->stash('eg_req_resolved_content_format') || 'json'; + return $bre->marc if ($resp_type eq 'xml'); + return MARC::Record->new_from_xml( $bre->marc, 'UTF-8', 'USMARC' )->as_usmarc if ($resp_type eq 'binary'); + return $bre; +} + +sub update_bre_parts { + my ($c, $ses, $bibid, $parts) = @_; + $parts ||= {}; + + my $e = new_editor(xact => 1, authtoken => $ses, personality => 'open-ils.pcrud'); + + my $bib = $e->retrieve_biblio_record_entry($bibid) || die 'Could not retrieve record'; + + for my $allowed_part ( qw/source owner share_depth/ ) { + if (exists $$parts{$allowed_part}) { # they want to set it to something... + if (defined $$parts{$allowed_part}) { # they want a value + $bib->$allowed_part($$parts{$allowed_part}); + } else { # they want to unset the value + $allowed_part = 'clear_'.$allowed_part; + $bib->$allowed_part; + } + } + } + + $e->update_biblio_record_entry($bib) || die 'Could not update record'; + $e->commit; + + return new_editor()->retrieve_biblio_record_entry($bibid); +} + +sub fetch_one_bib_display_fields { + my ($c, $bib, $map) = @_; + $map ||= '""=>"-1"'; + + return $U->simplereq( + 'open-ils.search', + 'open-ils.search.fetch.metabib.display_field.highlight.fleshed', + $map => $bib + ); +} + +sub fetch_one_bib_holdings { + my ($c, $bib, $limit, $offset) = @_; + + my $acn = new_editor()->search_asset_call_number([ + { record => $bib, + label => { '<>' => '##URI##'}, + deleted => 'f' + }, + { order_by => { acn => [qw/label_sortkey label owning_lib/] }, + flesh => 2, + flesh_fields => { + acn => [qw/copies prefix suffix/], + acp => [qw/status circ_lib location parts/] + } + } + ]) or die "cn tree fetch failed"; + + # flip CN->CP inside out + my @copies; + for my $cn (@$acn) { + push @copies, map { + $_->call_number($cn); + $_->circ_lib($_->circ_lib->id); + $_; + } grep { + !$U->is_true($_->deleted) + and $U->is_true($_->opac_visible) + and $U->is_true($_->status->opac_visible) + and $U->is_true($_->location->opac_visible) + and $U->is_true($_->circ_lib->opac_visible) + } sort { + $a->barcode cmp $b->barcode + } @{$cn->copies}; + $cn->clear_copies + } + + if ($limit) { + $offset ||= 0; + my $end_index = $offset + $limit - 1; + $end_index = scalar(@copies) - 1 if $end_index > scalar(@copies) - 1; + return [ @copies[$offset .. $end_index] ]; + } + + return \@copies; +} + +sub item_by_barcode { + my ($c, $barcode) = @_; + my $copy = new_editor()->search_asset_copy({deleted => 'f', barcode => $barcode})->[0]; + $c->res->code(404) if (!$copy); + return $copy; +} + +sub fetch_new_items { + my ($c, $limit, $offset, $age) = @_; + $offset ||= 0; + $limit ||= 100; + + my $filter = {deleted => 'f', active_date => {'!=' => undef}}; + $$filter{active_date} = { + '>=' => { + transform => 'age', + params => ['now'], + value => '-' . $age + } + } if ($age); + + my $order = {order_by => {acp => 'active_date DESC'}}; + if ($limit) { + $$order{limit} = $limit; + $$order{offset} = $offset; + } + + return new_editor()->search_asset_copy([ $filter, $order ]); +} + +sub delete_one_item { + my ($c, $ses, $barcode) = @_; + my $e = new_editor(authtoken=>$ses, xact=>1); + + my $copy = new_editor()->search_asset_copy({deleted => 'f', barcode => $barcode})->[0]; + do { $c->res->code(404); return {error=>"No copy found with barcode $barcode"}; } + unless ($copy); + + $evt = OpenILS::Application::Cat::AssetCommon->delete_copy( + $e, {all => 1}, $e->retrieve_asset_call_number($copy->call_number), $copy + ); + + if($evt) { + $e->rollback; + $c->res->code(400); + return $evt; + } + + $e->commit; + + return 1; +} + +sub create_or_update_one_item { + my ($c, $ses, $copy_blob, $barcode) = @_; + + my $copy; + my %parts = ( + loan_duration => undef, fine_level => undef, + copy_number => undef, + mint_condition => undef, age_protect => undef, + location => undef, circ_lib => undef, + deposit => undef, deposit_amount => undef, + circulate => undef, ref => undef, holdable => undef, + price => undef, cost => undef, + dummy_isbn => undef, dummy_author => undef, dummy_title => undef, + circ_as_type => undef, circ_modifier => undef, + opac_visible => undef + ); + + my $e = new_editor(authtoken=>$ses, xact=>1); + + if ($barcode) { + $copy = item_by_barcode($c, $barcode); + do { $c->res->code(404); return {error=>"No copy found with barcode $barcode"}; } + unless ($copy); + + OpenILS::OpenAPI::Controller::apply_blob_to_object($copy, $copy_blob, \%parts); + $copy->ischanged(1); + + $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies( + $e, {all => 1}, undef, [$copy] + ); + + if($evt) { + $e->rollback; + $c->res->code(400); + return $evt; + } + } else { + do { $c->res->code(400); return {error=>"No record supplied for item in 'bib' property"}; } + unless ($$copy_blob{bib} || $U->is_true($$copy_blob{precat})); + + do { $c->res->code(400); return {error=>"No call number supplied for item in 'call_number' property"}; } + unless ($$copy_blob{call_number} || $U->is_true($$copy_blob{precat})); + + $parts{status} = OILS_COPY_STATUS_IN_PROCESS; + $parts{loan_duration} = 2; + $parts{fine_level} = 2; + $parts{barcode} = undef; + + my $evt; + my $vol; + if ($U->is_true($$copy_blob{precat})) { + $vol = $e->retrieve_asset_call_number(-1); + } else { + ($vol, $evt) = OpenILS::Application::Cat::AssetCommon->find_or_create_volume( + $e, $$copy_blob{call_number}, $$copy_blob{bib}, $$copy_blob{circ_lib} + ); + + if($evt) { + $e->rollback; + $c->res->code(400); + return $evt; + } + } + + $copy = Fieldmapper::asset::copy->new; + OpenILS::OpenAPI::Controller::apply_blob_to_object($copy, $copy_blob, \%parts); + + if($evt = OpenILS::Application::Cat::AssetCommon->create_copy($e, $vol, $copy)) { + $e->rollback; + $c->res->code(400); + return $evt; + } + } + + $e->commit; + + return $e->retrieve_asset_copy($copy->id); +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/course.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/course.pm new file mode 100644 index 0000000000..51d7693b05 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/course.pm @@ -0,0 +1,60 @@ +package OpenILS::OpenAPI::Controller::course; +use OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Application::AppUtils; + +our $VERSION = 1; +our $U = "OpenILS::Application::AppUtils"; + +sub get_active_courses { + my ($c, $ses, $orgs) = @_; + + my $search = {is_archived => 'f'}; + if ($orgs) { + if (ref($orgs) and ref($orgs) eq 'ARRAY') { + $orgs = [grep {/^\d+$/} @$orgs]; # just IDs, please + $$search{owning_lib} = $orgs if (@$orgs); + } + } + + return new_editor(personality=>'open-ils.pcrud', authtoken=>$ses)->search_asset_course_module_course($search); +} + +sub get_course_detail { + my ($c, $ses, $c_id) = @_; + + my $course = new_editor(personality=>'open-ils.pcrud', authtoken=>$ses)->retrieve_asset_course_module_course([ + $c_id, { + flesh => 1, + flesh_fields => { + acmc => [qw/owning_lib terms_map members/], + acmcu => [qw/usr_role/], + acmtcm => [qw/term/] + } + } + ]); + + $course->members( [grep {$U->is_true($_->usr_role->is_public)} @{$course->members}] ); # only return is_public roles, if anything + + return $course; +} + +sub get_course_materials { + my ($c, $c_id) = @_; + + return $U->simplereq( + 'open-ils.courses', + 'open-ils.courses.course_materials.retrieve.fleshed.atomic', + { course => $c_id } + ); +} + +sub get_all_course_public_roles { + return $U->simplereq( # so close! param-mapped literal must be a scalar, for now, so we have to wrap this call + 'open-ils.courses', + 'open-ils.courses.course_users.retrieve', + { '!=' => undef } + ); +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/hold.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/hold.pm new file mode 100644 index 0000000000..8c03d26475 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/hold.pm @@ -0,0 +1,117 @@ +package OpenILS::OpenAPI::Controller::hold; +use OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Application::AppUtils; + +our $VERSION = 1; +our $U = "OpenILS::Application::AppUtils"; + +sub open_holds { + my ($c, $ses, $userid) = @_; + return $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.hold.details.batch.retrieve.atomic', + $ses, + $U->simplereq( + 'open-ils.circ', + 'open-ils.circ.holds.id_list.retrieve', + $ses, $userid + ) + ); +} + +sub cancel_user_hold { + my ($c, $ses, $userid, $holdid, $reason) = @_; + + my $old_hold = new_editor()->retrieve_action_hold_request($holdid); + if ($old_hold->usr ne $userid) { + $c->res->message('Cannot update hold for a different patron'); + $c->res->code(403); + return undef; + } + + my $res = $U->simplereq('open-ils.circ', 'open-ils.circ.hold.cancel', $ses, $holdid, $reason); + + if ($res and !ref($res)) { + $res = { errors => 0 }; + } else { + $c->res->code(403); + } + + return $res; +} + +sub update_user_hold { + my ($c, $ses, $userid, $holdid, $hold_patch) = @_; + + my $old_hold = new_editor()->retrieve_action_hold_request($holdid); + if ($old_hold->usr ne $userid) { + $c->res->message('Cannot update hold for a different patron'); + $c->res->code(403); + return undef; + } + + $$hold_patch{id} = $holdid; + my $res = $U->simplereq('open-ils.circ', 'open-ils.circ.hold.update', $ses, undef, $hold_patch); + + if ($res and !ref($res)) { + $res = { errors => 0 }; + } else { + $c->res->code(403); + } + + return $res; +} + +sub request_hold { + my ($c, $ses, $user_id, $hold_parts) = @_; + + if (!ref($hold_parts) or !($$hold_parts{bib} or $$hold_parts{copy}) or !$$hold_parts{pickup_lib}) { + $c->res->message('Invalid hold request'); + $c->res->code(403); + return undef; + } + + my $type = $$hold_parts{bib} ? 'T' : 'C'; + my $new_hold = { + patronid => $user_id, + hold_type => $type, + pickup_lib => $$hold_parts{pickup_lib}, + expire_time => $$hold_parts{expire_time}, + }; + + my $target = [ $$hold_parts{bib} || $$hold_parts{copy} ]; + my $result = $U->simplereq('open-ils.circ', 'open-ils.circ.holds.test_and_create.batch.override.atomic', $ses, $new_hold, $target)->[0]; + + $$result{error} = (ref($result) && ref($$result{result})) ? 1 : 0; + $c->res->code(403) if $$result{error}; + + return $result; +} + +sub fetch_user_hold { + my ($controller, $ses, $usrid, $holdid) = @_; + my $res = $U->simplereq( + 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', + $ses => $holdid + ); + + unless (ref($res) and $$res{hold} and $$res{hold}->usr == $usrid) { + $controller->res->code(403); + return undef; + } + return $res; +} + +sub valid_hold_pickup_locations { + my $e = new_editor(); + return $e->search_actor_org_unit({ + opac_visible => 't', + ou_type => $e->search_actor_org_unit_type( + [{ can_have_vols => 't' }], + { idlist => 1 } + ) + }); +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/org.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/org.pm new file mode 100644 index 0000000000..ee7d571798 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/org.pm @@ -0,0 +1,44 @@ +package OpenILS::OpenAPI::Controller::org; +use OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Application::AppUtils; + +our $VERSION = 1; +our $U = "OpenILS::Application::AppUtils"; + +sub full_tree { + my ($c) = @_; + return one_tree($c, new_editor()->search_actor_org_unit({parent_ou => undef})->[0]->id); +} + +sub one_tree { + my ($c, $org) = @_; + + return new_editor()->retrieve_actor_org_unit([ + $org, + {flesh => 100, flesh_fields => {aou => [qw/ou_type children/]}} + ]); +} + +sub one_org { + my ($c, $org) = @_; + return new_editor()->retrieve_actor_org_unit([ + $org, + {flesh => 1, flesh_fields => {aou => [qw/ou_type ill_address holds_address mailing_address billing_address/]}} + ]); +} + +sub flat_org_list { + my ($c, $fields, $ops, $values) = @_; + + my $where = ($fields and @$fields) ? + OpenILS::OpenAPI::Controller::where_clause_from_triples($fields, $ops, $values) : + {id => {'!=' => undef}}; + + return new_editor()->search_actor_org_unit([ + $where, + {flesh => 1, flesh_fields => {aou => [qw/ou_type ill_address holds_address mailing_address billing_address/]}} + ]); +} + +1; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/patron.pm b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/patron.pm new file mode 100644 index 0000000000..b8d18463f4 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/patron.pm @@ -0,0 +1,316 @@ +package OpenILS::OpenAPI::Controller::patron; +use OpenILS::OpenAPI::Controller; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenILS::Application::AppUtils; + +our $VERSION = 1; +our $U = "OpenILS::Application::AppUtils"; + +sub deliver_user { + my ($controller, $ses, $usr) = @_; + return OpenILS::OpenAPI::Controller::retrieve_one_object_via_pcrud( + $controller => $ses, + actor_user => $usr => {au => [qw/card cards addresses profile groups/]} + ); +} + +# New subroutine for fleshing the user-id-by-barcode-or-username method output +sub user_by_identifier_string { + my ($c, $ses, $barcode, $username) = @_; + + my $uid = $U->simplereq( + 'open-ils.actor', 'open-ils.actor.user.retrieve_id_by_barcode_or_username', + $ses, $barcode, $username + ); + + do { $c->res->code(404); return $uid; } if ref($uid); # ILS Event objects are refs + return deliver_user($c, $ses, $uid); +} + +sub find_users { + my ($c, $ses, $fields, $ops, $values, $limit, $offset) = @_; + $limit ||= 100; + $offset ||= 0; + + do { $c->res->code(400); return {error=>"No search terms provided"}; } + unless ($fields and @$fields); + + return new_editor(personality=>'open-ils.pcrud', authtoken=>$ses)->search_actor_user([ + { -and => [ + { deleted => 'f', active => 't' }, + OpenILS::OpenAPI::Controller::where_clause_from_triples($fields, $ops, $values) + ]}, + {limit => $limit, offset => $offset, flesh => 1, + flesh_fields => {au => [qw/card cards addresses profile groups/]}, + order_by => {au => [qw/usrname/]} + } + ]); +} + +sub confirm_circ_for_patron { + my ($circ_id, $must_match_user) = @_; + + my $e = new_editor(); + my $circ = $e->retrieve_action_circulation([ + $circ_id, {flesh => 1, flesh_fields => { circ => ['target_copy'] }} + ]); + + die 'invalid circulation id' unless $circ; + + if ($must_match_user) { # if passed, make sure the session user owns the circ + die 'invalid circulation id' unless $circ->usr == $must_match_user; + } + + return $circ; +} + +sub circ_result_with_error_wrapper { + my $c = shift; + my $res = shift; + $res = [$res] if (ref($res) ne 'ARRAY'); + + my $errors = [ grep { $$_{textcode} ne 'SUCCESS' } @$res ]; + $c->res->code(403) if @$errors; + return { errors => int(scalar(@$errors)), result => $res }; +} + +sub checkout_item { + my ($c, $ses, $userid, $copy_barcode) = @_; + return circ_result_with_error_wrapper($c, + $U->simplereq( + 'open-ils.circ', 'open-ils.circ.checkout.full', $ses, + { barcode => $copy_barcode, patron_id => $userid } + ) + ); +} + +sub checkin_circ { + my ($c, $ses, $circid, $must_match_user) = @_; + return circ_result_with_error_wrapper($c, + $U->simplereq( + 'open-ils.circ', 'open-ils.circ.checkin', $ses, + { barcode => confirm_circ_for_patron($circid, $must_match_user)->target_copy->barcode, force => 1, noop => 1 } + ) + ); +} + +sub renew_circ { + my ($c, $ses, $circid, $must_match_user) = @_; + my $circ = confirm_circ_for_patron($circid, $must_match_user); + return circ_result_with_error_wrapper($c, + $U->simplereq( + 'open-ils.circ', 'open-ils.circ.renew', $ses, + { copy_id => $circ->target_copy->id, patron_id => $circ->usr} + ) + ); +} + +sub update_user_parts { + my ($c, $ses, $update_parts) = @_; + + my $orig_pw = delete $$update_parts{current_password}; + + my %results; + for my $part ( keys %$update_parts ) { + my $res = $U->simplereq( + 'open-ils.actor', "open-ils.actor.user.$part.update", + $ses => $$update_parts{$part} => $orig_pw + ) or die "user update call failed"; + if (ref($res)) { + $results{$part} = { success => 0, error => $res }; + } else { + $results{$part} = { success => 1 }; + } + } + + return \%results; +}; + +sub circulation_history { + my ($c, $ses, $userid, $limit, $offset, $sort, $before, $after) = @_; + return transactions_by_state($c, $ses, $userid, 'all', $limit, $offset, $sort, $before, $after, 'circulation') +} + +sub usr_at_events { + my ($c, $ses, $user_id, $limit, $offset, $before, $after, $hooks, $event_id) = @_; + my $e = new_editor(authtoken => $ses); + + my $options = { + limit => $limit, + offset => $offset, + order_by => [{class=>'atev', field=>'run_time', direction=>'desc'}] + }; + + my $filter = {context_user => $user_id}; + if ($before and $after) { + $$filter{run_time} = {'between' => [$after, $before]}; + } elsif ($before) { + $$filter{run_time} = {'<' => $before}; + } elsif ($after) { + $$filter{run_time} = {'>' => $after}; + } + + $$filter{event_def} = [ + map {$_->id} @{$e->search_action_trigger_event_definition({hook=>$hooks})} + ] if ($hooks); + + if ($event_id) { + $$filter{id} = $event_id; + $$options{flesh} = 2; + $$options{flesh_fields} = { + atevdef => [qw/hook owner validator reactor cleanup_success cleanup_failure opt_in_setting env params/], + atev => [qw/event_def template_output error_output async_output context_library context_bib context_item/], + }; + } + + my $events = new_editor(authtoken => $ses)->search_action_trigger_event([ + $filter, $options + ]); + + return [ map {$_->id} @$events] unless $event_id; + return $$events[0]; +} + +sub transactions_by_state { + my ($c, $ses, $user_id, $state, $limit, $offset, $sort, $before, $after, $type) = @_; + $sort = 'desc' if ($sort and !grep {uc($sort) eq $_} qw/ASC DESC/); + + my $method = ''; + if ($state eq 'all') { + $method = 'open-ils.actor.user.transactions.history'; + } elsif (grep { $_ eq $state } qw/have_charge still_open have_balance have_bill have_bill_or_payment have_payment/) { + $method .= "open-ils.actor.user.transactions.history.$state"; + } + + die 'Invalid transaction type request' unless $method; + + my $options = { + limit => $limit, + offset => $offset, + sort => uc($sort) + }; + + my $filters = {}; + if ($before and $after) { + $$filter{xact_start} = {'between' => [$after, $before]}; + } elsif ($before) { + $$filter{xact_start} = {'<' => $before}; + } elsif ($after) { + $$filter{xact_start} = {'>' => $after}; + } + + return $U->simplereq( + 'open-ils.actor', $method, $ses, $user_id, $type, $filters, $options + ); +} + +sub update_usr_message { + my ($c, $ses, $ses_user, $userid, $message_id, $blob) = @_; + my $e = new_editor(personality => 'open-ils.pcrud', authtoken => $ses, xact=>1); + my $msg = $e->retrieve_actor_usr_message($message_id); + + if (!$msg or $msg->usr != $userid) { + $e->rollback; + $c->res->code(404); + return undef; + } + + my %parts = ( + title => undef, + message => undef, + stop_date => undef, + pub => undef, + deleted => undef + ); + + OpenILS::OpenAPI::Controller::apply_blob_to_object($msg, $blob, \%parts); + + $msg->editor($ses_user); + $msg->edit_date('now'); + + if (!$e->update_actor_usr_message($msg)) { + $e->rollback; + $c->res->code(403); + return undef; + } + + $msg = $e->retrieve_actor_usr_message($message_id); + $e->commit; + + return $msg; +} + +sub archive_usr_message { + my ($c, $ses, $ses_user, $userid, $message_id) = @_; + return update_usr_message( + $c, $ses, $ses_user, $userid, $message_id, {deleted => 't'} + ) ? 1 : undef; +} + +sub usr_messages { + my ($c, $ses, $userid, $pub_only, $message_id) = @_; + + my $filter = { + usr => $userid, + deleted => 'f', + '-or' => [ {stop_date => undef}, {stop_date => {'>' => 'now'}} ] + }; + $$filter{id} = $message_id if ($message_id); + + my $messages = new_editor( + personality => 'open-ils.pcrud', + authtoken => $ses + )->search_actor_usr_message( + $filter + ); + + $messages = [grep {$U->is_true($_->pub)} @$messages] + if ($pub_only); + + return [ map {$_->id} @$messages] unless $message_id; + return $$messages[0]; +} + +sub standing_penalties { + my ($c, $ses, $userid, $pub_only, $penalty_id) = @_; + + my $filter = { + usr => $userid, + '-or' => [ {stop_date => undef}, {stop_date => {'>' => 'now'}} ] + }; + $$filter{id} = $penalty_id if ($penalty_id); + + my $penalties = new_editor(personality => 'open-ils.pcrud', authtoken => $ses)->search_actor_user_standing_penalty([ + $filter, + { flesh => 1, flesh_fields => {ausp => ['standing_penalty','usr_message']} } + ]); + + $penalties = [grep {$U->is_true($_->standing_penalty->pub) and $U->is_true($_->usr_message->pub)} @$penalties] + if ($pub_only); + + return [ map {$_->id} @$penalties] unless $penalty_id; + return $penalties; +} + +sub usr_activity { + my ($c, $ses, $user_id, $maxage, $limit, $offset, $sort) = @_; + $sort = 'desc' if (!$sort or ($sort and !grep {uc($sort) eq $_} qw/ASC DESC/)); + $limit ||= 100; + $offset ||= 0; + + my $filters = {usr => $user_id}; + $$filters{event_time} = {'>' => $maxage} if ($maxage); + + return new_editor(personality=>'open-ils.pcrud', authtoken=>$ses)->search_actor_usr_activity([ + $filters, + { flesh => 1, + flesh_fields => {auact => ['etype']}, + limit => $limit, + offset => $offset, + order_by => [{class => auact => field => event_time => direction => $sort}] + } + ]); + +} + +1; diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index 5ddf319ccc..5e68ae5ef3 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -1093,45 +1093,6 @@ BEGIN END; $$ LANGUAGE PLPGSQL; -CREATE OR REPLACE FUNCTION - actor.verify_passwd(pw_usr INTEGER, pw_type TEXT, test_passwd TEXT) - RETURNS BOOLEAN AS $$ -DECLARE - pw_salt TEXT; -BEGIN - /* Returns TRUE if the password provided matches the in-db password. - * If the password type is salted, we compare the output of CRYPT(). - * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy - * 'main' passwords. - */ - - SELECT INTO pw_salt salt FROM actor.passwd - WHERE usr = pw_usr AND passwd_type = pw_type; - - IF NOT FOUND THEN - -- no such password - RETURN FALSE; - END IF; - - IF pw_salt IS NULL THEN - -- Password is unsalted, compare the un-CRYPT'ed values. - RETURN EXISTS ( - SELECT TRUE FROM actor.passwd WHERE - usr = pw_usr AND - passwd_type = pw_type AND - passwd = test_passwd - ); - END IF; - - RETURN EXISTS ( - SELECT TRUE FROM actor.passwd WHERE - usr = pw_usr AND - passwd_type = pw_type AND - passwd = CRYPT(test_passwd, pw_salt) - ); -END; -$$ STRICT LANGUAGE PLPGSQL; - -- Remove all activity entries by activity type, -- except the most recent entry per user. CREATE OR REPLACE FUNCTION diff --git a/Open-ILS/src/sql/Pg/610.schema.openapi.sql b/Open-ILS/src/sql/Pg/610.schema.openapi.sql new file mode 100755 index 0000000000..902ca6617e --- /dev/null +++ b/Open-ILS/src/sql/Pg/610.schema.openapi.sql @@ -0,0 +1,446 @@ +BEGIN; + +DROP SCHEMA IF EXISTS openapi CASCADE; +CREATE SCHEMA IF NOT EXISTS openapi; + +CREATE TABLE IF NOT EXISTS openapi.integrator ( + id INT PRIMARY KEY REFERENCES actor.usr (id), + enabled BOOL NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS openapi.json_schema_datatype ( + name TEXT PRIMARY KEY, + label TEXT NOT NULL UNIQUE, + description TEXT +); + +CREATE TABLE IF NOT EXISTS openapi.json_schema_format ( + name TEXT PRIMARY KEY, + label TEXT NOT NULL UNIQUE, + description TEXT +); + +CREATE TABLE IF NOT EXISTS openapi.rate_limit_definition ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, -- i18n + limit_interval INTERVAL NOT NULL, + limit_count INT NOT NULL +); +SELECT SETVAL('openapi.rate_limit_definition_id_seq'::TEXT, 100); + +CREATE TABLE IF NOT EXISTS openapi.endpoint ( + operation_id TEXT PRIMARY KEY, + path TEXT NOT NULL, + http_method TEXT NOT NULL CHECK (http_method IN ('get','put','post','delete','patch')), + security TEXT NOT NULL DEFAULT 'bearerAuth' CHECK (security IN ('bearerAuth','basicAuth','cookieAuth','paramAuth')), + summary TEXT NOT NULL, + method_source TEXT NOT NULL, -- perl module or opensrf application, tested by regex and assumes opensrf app name contains a "." + method_name TEXT NOT NULL, + method_params TEXT, -- eg, 'eg_auth_token hold' or 'eg_auth_token eg_user_id circ req.json' + active BOOL NOT NULL DEFAULT TRUE, + rate_limit INT REFERENCES openapi.rate_limit_definition (id), + CONSTRAINT path_and_method_once UNIQUE (path, http_method) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_param ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + name TEXT NOT NULL CHECK (name ~ '^\w+$'), + required BOOL NOT NULL DEFAULT FALSE, + in_part TEXT NOT NULL DEFAULT 'query' CHECK (in_part IN ('path','query','header','cookie')), + fm_type TEXT, + schema_type TEXT REFERENCES openapi.json_schema_datatype (name), + schema_format TEXT REFERENCES openapi.json_schema_format (name), + array_items TEXT REFERENCES openapi.json_schema_datatype (name), + default_value TEXT, + CONSTRAINT endpoint_and_name_once UNIQUE (endpoint, name), + CONSTRAINT format_requires_type CHECK (schema_format IS NULL OR schema_type IS NOT NULL), + CONSTRAINT array_items_requires_array_type CHECK (array_items IS NULL OR schema_type = 'array') +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_response ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + validate BOOL NOT NULL DEFAULT TRUE, + status INT NOT NULL DEFAULT 200, + content_type TEXT NOT NULL DEFAULT 'application/json', + description TEXT NOT NULL DEFAULT 'Success', + fm_type TEXT, + schema_type TEXT REFERENCES openapi.json_schema_datatype (name), + schema_format TEXT REFERENCES openapi.json_schema_format (name), + array_items TEXT REFERENCES openapi.json_schema_datatype (name), + CONSTRAINT endpoint_status_content_type_once UNIQUE (endpoint, status, content_type) +); + +CREATE TABLE IF NOT EXISTS openapi.perm_set ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); -- push sequence value +SELECT SETVAL('openapi.perm_set_id_seq'::TEXT, 1001); + +CREATE TABLE IF NOT EXISTS openapi.perm_set_perm_map ( + id SERIAL PRIMARY KEY, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_set_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint(operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set ( + name TEXT PRIMARY KEY, + description TEXT NOT NULL, + active BOOL NOT NULL DEFAULT TRUE, + rate_limit INT REFERENCES openapi.rate_limit_definition (id) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_user_rate_limit_map ( + id SERIAL PRIMARY KEY, + accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_accessor_once UNIQUE (accessor, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_user_rate_limit_map ( + id SERIAL PRIMARY KEY, + accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_accessor_once UNIQUE (accessor, endpoint_set) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_ip_rate_limit_map ( + id SERIAL PRIMARY KEY, + ip_range INET NOT NULL, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_ip_range_once UNIQUE (ip_range, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_ip_rate_limit_map ( + id SERIAL PRIMARY KEY, + ip_range INET NOT NULL, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_ip_range_once UNIQUE (ip_range, endpoint_set) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_endpoint_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_endpoint_once UNIQUE (endpoint_set, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_set_map ( + id SERIAL PRIMARY KEY, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_map ( + id SERIAL PRIMARY KEY, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.authen_attempt_log ( + request_id TEXT PRIMARY KEY, + attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ip_addr INET, + cred_user TEXT, + token TEXT +); +CREATE INDEX authen_cred_user_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, cred_user); +CREATE INDEX authen_ip_addr_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, ip_addr); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_access_attempt_log ( + request_id TEXT PRIMARY KEY, + attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + allowed BOOL NOT NULL, + ip_addr INET, + accessor INT REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + token TEXT +); +CREATE INDEX access_accessor_attempt_time_idx ON openapi.endpoint_access_attempt_log (accessor, attempt_time); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_dispatch_log ( + request_id TEXT PRIMARY KEY, + complete_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + error BOOL NOT NULL +); + +CREATE OR REPLACE FUNCTION openapi.find_default_endpoint_rate_limit (target_endpoint TEXT) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + -- Default rate limits can be applied at the endpoint or endpoint_set level; + -- endpoint overrides endpoint_set, and we choose the most restrictive from + -- the set if we have to look there. + SELECT d.* INTO def_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint e ON (e.rate_limit = d.id) + WHERE e.operation_id = target_endpoint; + + IF NOT FOUND THEN + SELECT d.* INTO def_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set es ON (es.rate_limit = d.id) + JOIN openapi.endpoint_set_endpoint_map m ON (es.name = m.endpoint_set AND m.endpoint = target_endpoint) + -- This ORDER BY calculates the avg time between requests the user would have to wait to perfectly + -- avoid rate limiting. So, a bigger wait means it's more restrictive. We take the most restrictive + -- set-applied one. + ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + -- If there's no default for the endpoint or set, we provide 1/sec. + IF NOT FOUND THEN + def_rl.limit_interval := '1 second'::INTERVAL; + def_rl.limit_count := 1; + END IF; + + RETURN def_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.find_user_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_u_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + SELECT d.* INTO def_u_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_user_rate_limit_map e ON (e.rate_limit = d.id) + WHERE e.endpoint = target_endpoint + AND e.accessor = accessing_usr; + + IF NOT FOUND THEN + SELECT d.* INTO def_u_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set_user_rate_limit_map e ON (e.rate_limit = d.id AND e.accessor = accessing_usr) + JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint) + ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + RETURN def_u_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.find_ip_addr_endpoint_rate_limit (target_endpoint TEXT, from_ip_addr INET) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_i_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + SELECT d.* INTO def_i_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_ip_rate_limit_map e ON (e.rate_limit = d.id) + WHERE e.endpoint = target_endpoint + AND e.ip_range && from_ip_addr + -- For IPs, we order first by the size of the ranges that we + -- matched (mask length), most specific (smallest block of IPs) + -- first, then by the restrictiveness of the limit, more restrictive first. + ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + + IF NOT FOUND THEN + SELECT d.* INTO def_i_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set_ip_rate_limit_map e ON (e.rate_limit = d.id AND ip_range && from_ip_addr) + JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint) + ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + RETURN def_i_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.check_generic_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; + def_u_rl openapi.rate_limit_definition%ROWTYPE; + def_i_rl openapi.rate_limit_definition%ROWTYPE; + u_wait INT; + i_wait INT; +BEGIN + def_rl := openapi.find_default_endpoint_rate_limit(target_endpoint); + + IF accessing_usr IS NOT NULL THEN + def_u_rl := openapi.find_user_endpoint_rate_limit(target_endpoint, accessing_usr); + END IF; + + IF from_ip_addr IS NOT NULL THEN + def_i_rl := openapi.find_ip_addr_endpoint_rate_limit(target_endpoint, from_ip_addr); + END IF; + + -- Now we test the user-based and IP-based limits in their focused way... + IF def_u_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.accessor = accessing_usr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + IF def_i_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + -- If there are no user-specific or IP-based limit + -- overrides; check endpoint-wide limits for user, + -- then IP, and if we were passed neither, then limit + -- endpoint access for all users. Better to lock it + -- all down than to set the servers on fire. + IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN + IF accessing_usr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.accessor = accessing_usr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSIF from_ip_addr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSE -- we have no user and no IP, global per-endpoint rate limit? + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.endpoint ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + END IF; + END IF; + + -- Send back the largest required wait time, or NULL for no restriction + u_wait := GREATEST(u_wait,i_wait); + IF u_wait > 0 THEN + RETURN u_wait; + END IF; + + RETURN NULL; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.check_auth_endpoint_rate_limit (accessing_usr TEXT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; + def_u_rl openapi.rate_limit_definition%ROWTYPE; + def_i_rl openapi.rate_limit_definition%ROWTYPE; + u_wait INT; + i_wait INT; +BEGIN + def_rl := openapi.find_default_endpoint_rate_limit('authenticateUser'); + + IF accessing_usr IS NOT NULL THEN + SELECT (openapi.find_user_endpoint_rate_limit('authenticateUser', u.id)).* INTO def_u_rl + FROM actor.usr u + WHERE u.usrname = accessing_usr; + END IF; + + IF from_ip_addr IS NOT NULL THEN + def_i_rl := openapi.find_ip_addr_endpoint_rate_limit('authenticateUser', from_ip_addr); + END IF; + + -- Now we test the user-based and IP-based limits in their focused way... + IF def_u_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.cred_user = accessing_usr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + IF def_i_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + -- If there are no user-specific or IP-based limit + -- overrides; check endpoint-wide limits for user, + -- then IP, and if we were passed neither, then limit + -- endpoint access for all users. Better to lock it + -- all down than to set the servers on fire. + IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN + IF accessing_usr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.cred_user = accessing_usr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSIF from_ip_addr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSE -- we have no user and no IP, global auth attempt rate limit? + SELECT CEIL(EXTRACT(EPOCH FROM (l.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM openapi.authen_attempt_log l + WHERE l.attempt_time > NOW() - def_rl.limit_interval + ORDER BY l.attempt_time DESC + LIMIT 1 OFFSET def_rl.limit_count; + END IF; + END IF; + + -- Send back the largest required wait time, or NULL for no restriction + u_wait := GREATEST(u_wait,i_wait); + IF u_wait > 0 THEN + RETURN u_wait; + END IF; + + RETURN NULL; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index c36d191c51..8f34ce9ced 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -2047,6 +2047,20 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES 'Allow setting and unsetting hold from top of hold queue (cut in line)', 'ppl', 'description')) ; +INSERT INTO permission.perm_list (id,code) VALUES + (677,'ADMIN_OPENAPI'), + (678,'API_LOGIN'), + (679,'REST.api'), + (680,'REST.api.patrons'), + (681,'REST.api.orgs'), + (682,'REST.api.bibs'), + (683,'REST.api.items'), + (684,'REST.api.holds'), + (685,'REST.api.collections'), + (686,'REST.api.courses'), + (687,'group_application.api_integrator') +ON CONFLICT DO NOTHING; + SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000); INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES @@ -2083,6 +2097,22 @@ INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, u (14, oils_i18n_gettext(14, 'Data Review', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.data_review'); INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES (15, oils_i18n_gettext(15, 'Volunteers', 'pgt', 'name'), 3, NULL, '3 years', TRUE, 'group_application.user.staff.volunteers'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (16, oils_i18n_gettext(16, 'API Integrator', 'pgt', 'name'), 1, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (17, oils_i18n_gettext(17, 'Patron API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (18, oils_i18n_gettext(18, 'Org Unit API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (19, oils_i18n_gettext(19, 'Bib Record API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (20, oils_i18n_gettext(20, 'Item Record API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (21, oils_i18n_gettext(21, 'Holds API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (22, oils_i18n_gettext(22, 'Debt Collection API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); +INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES + (23, oils_i18n_gettext(23, 'Course Reserves API', 'pgt', 'name'), 16, NULL, '3 years', TRUE, 'group_application.api_integrator'); SELECT SETVAL('permission.grp_tree_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_tree)); @@ -2098,7 +2128,7 @@ INSERT INTO permission.grp_penalty_threshold (grp,org_unit,penalty,threshold) SELECT SETVAL('permission.grp_penalty_threshold_id_seq'::TEXT, (SELECT MAX(id) FROM permission.grp_penalty_threshold)); --- Add basic user permissions to the Users group +-- Add basic user permissions to the Staff and Patrons groups INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) SELECT @@ -2108,7 +2138,7 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) permission.perm_list perm, actor.org_unit_type aout WHERE - pgt.name = 'Users' AND + pgt.name IN ('Staff','Patrons') AND aout.name = 'Consortium' AND perm.code IN ( 'COPY_CHECKIN', @@ -2122,6 +2152,25 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) 'user_request.create' ); +-- Add baselin API Integrator permissions + +INSERT INTO permission.grp_perm_map (grp,perm,depth) + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='API Integrator' AND p.code IN ('API_LOGIN','REST.api') + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Patron API' AND p.code = 'REST.api.patrons' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Org Unit API' AND p.code = 'REST.api.orgs' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Bib Record API' AND p.code = 'REST.api.bibs' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Item Record API' AND p.code = 'REST.api.items' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Holds API' AND p.code = 'REST.api.holds' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Debt Collection API' AND p.code = 'REST.api.collections' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Course Reserves API' AND p.code = 'REST.api.courses' +ON CONFLICT DO NOTHING; -- Add basic user permissions to the Data Review group @@ -25064,3 +25113,905 @@ VALUES ( 'bool' ); +------- OpenAPI supporting data ------ + +INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count) + VALUES ('api', 'OpenAPI Integration Password', TRUE, 'bf', 10) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.json_schema_datatype (name,label) VALUES + ('boolean','Boolean'), + ('string','String'), + ('integer','Integer'), + ('number','Number'), + ('array','Array'), + ('object','Object') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.json_schema_format (name,label) VALUES + ('date-time','Timestamp'), + ('date','Date'), + ('time','Time'), + ('interval','Interval'), + ('email','Email Address'), + ('uri','URI'), + ('identifier','Opaque Identifier'), + ('money','Money'), + ('float','Floating Point Number'), + ('int64','Large Integer'), + ('password','Password') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.rate_limit_definition (id, name, limit_interval, limit_count) VALUES + (1, 'Once per second', '1 second', 1), + (2, 'Ten per minute', '1 minute', 10), + (3, 'One hunderd per hour', '1 hour', 100), + (4, 'One thousand per hour', '1 hour', 1000), + (5, 'One thousand per 24 hour period', '24 hours', 1000), + (6, 'Ten thousand per 24 hour period', '24 hours', 10000), + (7, 'Unlimited', '1 second', 1000000), + (8, 'One hundred per second', '1 second', 100) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.perm_set (id, name) VALUES + (1,'Self - API only'), + (2,'Patrons - API only'), + (3,'Orgs - API only'), + (4,'Bibs - API only'), + (5,'Items - API only'), + (6,'Holds - API only'), + (7,'Collections - API only'), + (8,'Courses - API only'), + (101,'Self - standard permissions'), + (102,'Patrons - standard permissions'), + (103,'Orgs - standard permissions'), + (104,'Bibs - standard permissions'), + (105,'Items - standard permissions'), + (106,'Holds - standard permissions'), + (107,'Collections - standard permissions'), + (108,'Courses - standard permissions') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.perm_set_perm_map (perm_set, perm) + SELECT 1, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api') + UNION + SELECT 2, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.patrons') + UNION + SELECT 3, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.orgs') + UNION + SELECT 4, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.bibs') + UNION + SELECT 5, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.items') + UNION + SELECT 6, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.holds') + UNION + SELECT 7, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.collections') + UNION + SELECT 8, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.cources') + UNION + SELECT 101, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 102, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 103, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 104, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 105, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 106, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 107, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 108, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint_set (name, description) VALUES + ('self', 'Methods for retrieving and manipulating your own user account information'), + ('orgs', 'Methods for retrieving and manipulating organizational unit information'), + ('patrons', 'Methods for retrieving and manipulating patron information'), + ('holds', 'Methods for accessing and manipulating hold data'), + ('collections', 'Methods for accessing and manipulating patron debt collections data'), + ('bibs', 'Methods for accessing and manipulating bibliographic records and related data'), + ('items', 'Methods for accessing and manipulating barcoded item records'), + ('courses', 'Methods for accessing and manipulating course reserve data') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES + ('self', 1), ('self', 101), + ('patrons', 2), ('patrons', 102), + ('orgs', 3), ('orgs', 103), + ('bibs', 4), ('bibs', 104), + ('items', 5), ('items', 105), + ('holds', 6), ('holds', 106), + ('collections', 7), ('collections', 107), + ('courses', 8), ('courses', 108) +ON CONFLICT DO NOTHING; + + +----------- OpenAPI endpoint configuration ----------------- + +-- ===== authentication +INSERT INTO openapi.endpoint (operation_id, path, security, http_method, summary, method_source, method_name, method_params, rate_limit) VALUES + ('authenticateUser', '/self/auth', 'basicAuth', 'get', 'Authenticate API user', 'OpenILS::OpenAPI::Controller', 'authenticateUser', 'param.u param.p param.t', 8), + ('logoutUser', '/self/auth', 'bearerAuth', 'delete', 'Logout API user', 'open-ils.auth', 'open-ils.auth.session.delete', 'eg_auth_token', 8) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('authenticateUser','u','query','string',NULL,NULL), + ('authenticateUser','p','query','string','password',NULL), + ('authenticateUser','t','query','string',NULL,'api') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('authenticateUser','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('authenticateUser','text/plain') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('logoutUser','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('logoutUser','text/plain') ON CONFLICT DO NOTHING; + +-- ===== self-service +-- get/update me +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfProfile', '/self/me', 'get', 'Return patron/user record for logged in user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token eg_user_id'), + ('selfUpdateParts', '/self/me', 'patch', 'Update portions of the logged in user''s record', 'OpenILS::OpenAPI::Controller::patron', 'update_user_parts', 'eg_auth_token req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveSelfProfile','au') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('selfUpdateParts','object') ON CONFLICT DO NOTHING; + +-- get my standing penalties +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'selfActivePenalties', + '/self/standing_penalties', + 'get', + 'Produces patron-visible penalty details for the authorized account', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token eg_user_id "1"' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('selfActivePenalties','array','object') ON CONFLICT DO NOTHING; -- array of fleshed ausp + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'selfPenalty', + '/self/standing_penalty/:penaltyid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token eg_user_id "1" param.penaltyid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('selfPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING; + +-- manage my holds +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfHolds', '/self/holds', 'get', 'Return unfilled holds for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token eg_user_id'), + ('requestSelfHold', '/self/holds', 'post', 'Request a hold for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token eg_user_id req.json'), + ('retrieveSelfHold', '/self/hold/:hold', 'get', 'Retrieve one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token eg_user_id param.hold'), + ('updateSelfHold', '/self/hold/:hold', 'patch', 'Update one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token eg_user_id param.hold req.json'), + ('cancelSelfHold', '/self/hold/:hold', 'delete', 'Cancel one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token eg_user_id param.hold "6"') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrieveSelfHold','hold','path','integer',TRUE), + ('updateSelfHold','hold','path','integer',TRUE), + ('cancelSelfHold','hold','path','integer',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfHolds','array','object') ON CONFLICT DO NOTHING; + +-- general xact list +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfXacts', + '/self/transactions/:state', + 'get', + 'Produces a list of transactions of the logged in user', + 'OpenILS::OpenAPI::Controller::patron', + 'transactions_by_state', + 'eg_auth_token eg_user_id state param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrieveSelfXacts','state','path','string',NULL,NULL,TRUE), + ('retrieveSelfXacts','limit','query','integer',NULL,NULL,FALSE), + ('retrieveSelfXacts','offset','query','integer',NULL,'0',FALSE), + ('retrieveSelfXacts','sort','query','string',NULL,'desc',FALSE), + ('retrieveSelfXacts','before','query','string','date-time',NULL,FALSE), + ('retrieveSelfXacts','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfXacts','array','object') ON CONFLICT DO NOTHING; + +-- general xact detail +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfXact', + '/self/transaction/:id', + 'get', + 'Details of one transaction for the logged in user', + 'open-ils.actor', + 'open-ils.actor.user.transaction.fleshed.retrieve', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveSelfXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfCircs', '/self/checkouts', 'get', 'Open Circs for the logged in user', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token eg_user_id'), + ('requestSelfCirc', '/self/checkouts', 'post', 'Attempt a circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token eg_user_id req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircs','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfCircHistory', + '/self/checkouts/history', + 'get', + 'Historical Circs for logged in user', + 'OpenILS::OpenAPI::Controller::patron', + 'circulation_history', + 'eg_auth_token eg_user_id param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('retrieveSelfCircHistory','limit','query','integer',NULL,NULL), + ('retrieveSelfCircHistory','offset','query','integer',NULL,'0'), + ('retrieveSelfCircHistory','sort','query','string',NULL,'desc'), + ('retrieveSelfCircHistory','before','query','string','date-time',NULL), + ('retrieveSelfCircHistory','after','query','string','date-time',NULL) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircHistory','array','object') ON CONFLICT DO NOTHING; + + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfCirc', '/self/checkout/:id', 'get', 'Retrieve one circulation for the logged in user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'), + ('renewSelfCirc', '/self/checkout/:id', 'put', 'Renew one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id eg_user_id'), + ('checkinSelfCirc', '/self/checkout/:id', 'delete', 'Check in one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id eg_user_id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrieveSelfCirc','id','path','integer',TRUE), + ('renewSelfCirc','id','path','integer',TRUE), + ('checkinSelfCirc','id','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +-- bib, item, and org methods +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOrgList', + '/org_units', + 'get', + 'List of org units', + 'OpenILS::OpenAPI::Controller::org', + 'flat_org_list', + 'every_param.field every_param.comparison every_param.value' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES + ('retrieveOrgList','field','query','string'), + ('retrieveOrgList','comparison','query','string'), + ('retrieveOrgList','value','query','string') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOrgList','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneOrg', + '/org_unit/:id', + 'get', + 'One org unit', + 'OpenILS::OpenAPI::Controller::org', + 'one_org', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneOrg','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneOrg','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name) VALUES ( + 'retrieveFullOrgTree', + '/org_tree', + 'get', + 'Full hierarchical tree of org units', + 'OpenILS::OpenAPI::Controller::org', + 'full_tree' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveFullOrgTree','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePartialOrgTree', + '/org_tree/:id', + 'get', + 'Partial hierarchical tree of org units starting from a specific org unit', + 'OpenILS::OpenAPI::Controller::org', + 'one_tree', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePartialOrgTree','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePartialOrgTree','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'createOneBib', + '/bibs', + 'post', + 'Attempts to create a bibliographic record using MARCXML passed as the request content', + 'open-ils.cat', + 'open-ils.cat.biblio.record.xml.create', + 'eg_auth_token req.text param.sourcename' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('createOneBib','sourcename','query','string') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('createOneBib','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateOneBib', + '/bib/:id', + 'put', + 'Attempts to update a bibliographic record using MARCXML passed as the request content', + 'open-ils.cat', + 'open-ils.cat.biblio.record.marc.replace', + 'eg_auth_token param.id req.text param.sourcename' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('updateOneBib','sourcename','query','string') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('updateOneBib','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'deleteOneBib', + '/bib/:id', + 'delete', + 'Attempts to delete a bibliographic record', + 'open-ils.cat', + 'open-ils.cat.biblio.record_entry.delete', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,validate) VALUES ('deleteOneBib','integer',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateBREParts', + '/bib/:id', + 'patch', + 'Attempts to update the biblio.record_entry metadata surrounding a bib record', + 'OpenILS::OpenAPI::Controller::bib', + 'update_bre_parts', + 'eg_auth_token param.id req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateBREParts','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateBREParts','bre') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneBib', + '/bib/:id', + 'get', + 'Retrieve a bibliographic record, either full biblio::record_entry object, or just the MARCXML', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneBib','bre') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/xml') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/octet-stream') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneBibHoldings', + '/bib/:id/holdings', + 'get', + 'Retrieve the holdings data for a bibliographic record', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib_holdings', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value,required) VALUES + ('retrieveOneBibHoldings','id','path','integer',NULL,TRUE), + ('retrieveOneBibHoldings','limit','query','integer',NULL,FALSE), + ('retrieveOneBibHoldings','offset','query','integer','0',FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOneBibHoldings','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'bibDisplayFields', + '/bib/:id/display_fields', + 'get', + 'Retrieve display-related data for a bibliographic record', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib_display_fields', + 'param.id req.text' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('bibDisplayFields','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('bibDisplayFields','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'newItems', + '/items/fresh', + 'get', + 'Retrieve a list of newly added items', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_new_items', + 'param.limit param.offset param.maxage' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','limit','query','integer','0') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','offset','query','integer','100') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format) VALUES ('newItems','maxage','query','string','interval') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('newItems','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveItem', + '/item/:barcode', + 'get', + 'Retrieve one item by its barcode', + 'OpenILS::OpenAPI::Controller::bib', + 'item_by_barcode', + 'param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('retrieveItem','Item Lookup Failed','404','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'createItem', + '/items', + 'post', + 'Create an item record', + 'OpenILS::OpenAPI::Controller::bib', + 'create_or_update_one_item', + 'eg_auth_token req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('createItem','Item Creation Failed','400') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('createItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateItem', + '/item/:barcode', + 'patch', + 'Update a restricted set of item record fields', + 'OpenILS::OpenAPI::Controller::bib', + 'create_or_update_one_item', + 'eg_auth_token req.json param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('updateItem','Item Update Failed','400') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'deleteItem', + '/item/:barcode', + 'delete', + 'Delete one item record', + 'OpenILS::OpenAPI::Controller::bib', + 'delete_one_item', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('deleteItem','Item Deletion Failed','404','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('deleteItem','boolean') ON CONFLICT DO NOTHING; + +-- === patron (non-self) methods +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'searchPatrons', + '/patrons', + 'get', + 'List of patrons matching requested conditions', + 'OpenILS::OpenAPI::Controller::patron', + 'find_users', + 'eg_auth_token every_param.field every_param.comparison every_param.value param.limit param.offset' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES + ('searchPatrons','field','query','string'), + ('searchPatrons','comparison','query','string'), + ('searchPatrons','value','query','string') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('searchPatrons','offset','query','integer',NULL,'0'), + ('searchPatrons','limit','query','integer',NULL,'100') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('searchPatrons','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'verifyUserCredentials', + '/patrons/verify', + 'get', + 'Verify the credentials for a user account', + 'open-ils.actor', + 'open-ils.actor.verify_user_password', + 'eg_auth_token param.barcode param.usrname "" param.password' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('verifyUserCredentials','barcode','query','string',NULL,FALSE), + ('verifyUserCredentials','usrname','query','string',NULL,FALSE), + ('verifyUserCredentials','password','query','string','password',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('verifyUserCredentials','boolean') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronProfile', '/patron/:userid', 'get', 'Return patron/user record for the requested user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token param.userid') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronProfile','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePatronProfile','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronIdByCardBarcode', + '/patrons/by_barcode/:barcode/id', + 'get', + 'Retrieve patron id by barcode', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByCardBarcode','integer') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronIdByUsername', + '/patrons/by_username/:username/id', + 'get', + 'Retrieve patron id by username', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', + 'eg_auth_token "" param.username' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByUsername','integer') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronByCardBarcode', + '/patrons/by_barcode/:barcode', + 'get', + 'Retrieve patron by barcode', + 'OpenILS::OpenAPI::Controller::patron', + 'user_by_identifier_string', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByCardBarcode','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronByUsername', + '/patrons/by_username/:username', + 'get', + 'Retrieve patron by username', + 'OpenILS::OpenAPI::Controller::patron', + 'user_by_identifier_string', + 'eg_auth_token "" param.username' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByUsername','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronCircHistory', + '/patron/:userid/checkouts/history', + 'get', + 'Historical Circs for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'circulation_history', + 'eg_auth_token param.userid param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrievePatronCircHistory','userid','path','integer',NULL,NULL,TRUE), + ('retrievePatronCircHistory','limit','query','integer',NULL,NULL,FALSE), + ('retrievePatronCircHistory','offset','query','integer',NULL,'0',FALSE), + ('retrievePatronCircHistory','sort','query','string',NULL,'desc',FALSE), + ('retrievePatronCircHistory','before','query','string','date-time',NULL,FALSE), + ('retrievePatronCircHistory','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircHistory','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronHolds', '/patron/:userid/holds', 'get', 'Retrieve unfilled holds for a patron', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token param.userid'), + ('requestPatronHold', '/patron/:userid/holds', 'post', 'Request a hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token param.userid req.json'), + ('retrievePatronHold','/patron/:userid/hold/:hold', 'get', 'Retrieve one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token param.userid param.hold'), + ('updatePatronHold', '/patron/:userid/hold/:hold', 'patch', 'Update one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token param.userid param.hold req.json'), + ('cancelPatronHold', '/patron/:userid/hold/:hold', 'delete', 'Cancel one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token param.userid param.hold "6"') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrievePatronHolds','userid','path','integer',TRUE), + ('retrievePatronHold','userid','path','integer',TRUE), + ('retrievePatronHold','hold','path','integer',TRUE), + ('requestPatronHold','userid','path','integer',TRUE), + ('updatePatronHold','userid','path','integer',TRUE), + ('updatePatronHold','hold','path','integer',TRUE), + ('cancelPatronHold','userid','path','integer',TRUE), + ('cancelPatronHold','hold','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveHoldPickupLocations', '/holds/pickup_locations', 'get', 'Retrieve all valid hold/reserve pickup locations', 'OpenILS::OpenAPI::Controller::hold', 'valid_hold_pickup_locations', '') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveHoldPickupLocations','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveHold', '/hold/:hold', 'get', 'Retrieve one hold object', 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', 'eg_auth_token param.hold') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveHold','hold','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronXacts', + '/patron/:userid/transactions/:state', + 'get', + 'Produces a list of transactions of the specified user', + 'OpenILS::OpenAPI::Controller::patron', + 'transactions_by_state', + 'eg_auth_token param.userid state param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrievePatronXacts','userid','path','integer',NULL,NULL,TRUE), + ('retrievePatronXacts','state','path','string',NULL,NULL,TRUE), + ('retrievePatronXacts','limit','query','integer',NULL,NULL,FALSE), + ('retrievePatronXacts','offset','query','integer',NULL,'0',FALSE), + ('retrievePatronXacts','sort','query','string',NULL,'desc',FALSE), + ('retrievePatronXacts','before','query','string','date-time',NULL,FALSE), + ('retrievePatronXacts','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronXacts','array','object') ON CONFLICT DO NOTHING; + +-- general xact detail +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronXact', + '/patron/:userid/transaction/:id', + 'get', + 'Details of one transaction for the specified user', + 'open-ils.actor', + 'open-ils.actor.user.transaction.fleshed.retrieve', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING; + + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronCircs', '/patron/:userid/checkouts', 'get', 'Open Circs for a patron', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token param.userid'), + ('requestPatronCirc', '/patron/:userid/checkouts', 'post', 'Attempt a circulation for a patron', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token param.userid req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronCircs','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('requestPatronCirc','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircs','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronCirc', '/patron/:userid/checkout/:id', 'get', 'Retrieve one circulation for the specified user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'), + ('renewPatronCirc', '/patron/:userid/checkout/:id', 'put', 'Renew one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id param.userid'), + ('checkinPatronCirc', '/patron/:userid/checkout/:id', 'delete', 'Check in one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id param.userid') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrievePatronCirc','userid','path','integer',TRUE), + ('retrievePatronCirc','id','path','integer',TRUE), + ('renewPatronCirc','userid','path','integer',TRUE), + ('renewPatronCirc','id','path','integer',TRUE), + ('checkinPatronCirc','userid','path','integer',TRUE), + ('checkinPatronCirc','id','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronATEvents', + '/patron/:userid/triggered_events', + 'get', + 'Retrieve a list of A/T events for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_at_events', + 'eg_auth_token param.userid param.limit param.offset param.before param.after every_param.hook' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('patronATEvents','userid','path','integer',NULL,NULL,TRUE), + ('patronATEvents','limit','query','integer',NULL,'100',FALSE), + ('patronATEvents','offset','query','integer',NULL,'0',FALSE), + ('patronATEvents','before','query','string','date-time',NULL,FALSE), + ('patronATEvents','after','query','string','date-time',NULL,FALSE), + ('patronATEvents','hook','query','string',NULL,NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronATEvents','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronATEvent', + '/patron/:userid/triggered_event/:eventid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_at_events', + 'eg_auth_token param.userid "1" "0" "" "" "" param.eventid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','eventid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActivePenalties', + '/patron/:userid/standing_penalties', + 'get', + 'Retrieve all penalty details for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token param.userid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActivePenalties','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivePenalties','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronPenalty', + '/patron/:userid/standing_penalty/:penaltyid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token param.userid "0" param.penaltyid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActiveMessages', + '/patron/:userid/messages', + 'get', + 'Retrieve all active message ids for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_messages', + 'eg_auth_token param.userid "0"' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActiveMessages','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActiveMessages','array','integer') ON CONFLICT DO NOTHING; -- array of aum ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessage', + '/patron/:userid/message/:msgid', + 'get', + 'Retrieve one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_messages', + 'eg_auth_token param.userid "0" param.msgid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessage','aum') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessageUpdate', + '/patron/:userid/message/:msgid', + 'patch', + 'Update one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'update_usr_message', + 'eg_auth_token eg_user_id param.userid param.msgid req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessageUpdate','aum') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessageArchive', + '/patron/:userid/message/:msgid', + 'delete', + 'Archive one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'archive_usr_message', + 'eg_auth_token eg_user_id param.userid param.msgid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronMessageArchive','boolean') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActivityLog', + '/patron/:userid/activity', + 'get', + 'Retrieve patron activity (authen, authz, etc)', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_activity', + 'eg_auth_token param.userid param.maxage param.limit param.offset param.sort' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('patronActivityLog','userid','path','integer',NULL,NULL,TRUE), + ('patronActivityLog','limit','query','integer',NULL,'100',FALSE), + ('patronActivityLog','offset','query','integer',NULL,'0',FALSE), + ('patronActivityLog','sort','query','string',NULL,'desc',FALSE), + ('patronActivityLog','maxage','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivityLog','array','object') ON CONFLICT DO NOTHING; + +------- collections +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPatronsOfInterest', + '/collections/:shortname/users_of_interest', + 'get', + 'List of patrons to consider for collections based on the search criteria provided.', + 'open-ils.collections', + 'open-ils.collections.users_of_interest.retrieve', + 'eg_auth_token param.fine_age param.fine_amount param.shortname' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPatronsOfInterest','shortname','path','string',NULL,TRUE), + ('collectionsPatronsOfInterest','fine_age','query','integer',NULL,TRUE), + ('collectionsPatronsOfInterest','fine_amount','query','string','money',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterest','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPatronsOfInterestWarning', + '/collections/:shortname/users_of_interest/warning', + 'get', + 'List of patrons with the PATRON_EXCEEDS_COLLECTIONS_WARNING penalty to consider for collections based on the search criteria provided.', + 'open-ils.collections', + 'open-ils.collections.users_of_interest.warning_penalty.retrieve', + 'eg_auth_token param.shortname param.min_age param.max_age' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPatronsOfInterestWarning','shortname','path','string',NULL,TRUE), + ('collectionsPatronsOfInterestWarning','min_age','query','string','date-time',FALSE), + ('collectionsPatronsOfInterestWarning','max_age','query','string','date-time',FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterestWarning','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsGetPatronDetail', + '/patron/:usrid/collections/:shortname', + 'get', + 'Get collections-related transaction details for a patron.', + 'open-ils.collections', + 'open-ils.collections.user_transaction_details.retrieve', + 'eg_auth_token param.start param.end param.shortname every_param.usrid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsGetPatronDetail','usrid','path','integer',NULL,TRUE), + ('collectionsGetPatronDetail','shortname','path','string',NULL,TRUE), + ('collectionsGetPatronDetail','start','query','string','date-time',TRUE), + ('collectionsGetPatronDetail','end','query','string','date-time',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsGetPatronDetail','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPutPatronInCollections', + '/patron/:usrid/collections/:shortname', + 'post', + 'Put patron into collections.', + 'open-ils.collections', + 'open-ils.collections.put_into_collections', + 'eg_auth_token param.usrid param.shortname param.fee param.note' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPutPatronInCollections','usrid','path','integer',NULL,TRUE), + ('collectionsPutPatronInCollections','shortname','path','string',NULL,TRUE), + ('collectionsPutPatronInCollections','fee','query','string','money',FALSE), + ('collectionsPutPatronInCollections','note','query','string',NULL,FALSE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsRemovePatronFromCollections', + '/patron/:usrid/collections/:shortname', + 'delete', + 'Remove patron from collections.', + 'open-ils.collections', + 'open-ils.collections.remove_from_collections', + 'eg_auth_token param.usrid param.shortname' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('collectionsRemovePatronFromCollections','usrid','path','integer',TRUE), + ('collectionsRemovePatronFromCollections','shortname','path','string',TRUE) +ON CONFLICT DO NOTHING; + +------- courses +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('activeCourses', '/courses', 'get', 'Retrieve all courses used for course material reservation', 'OpenILS::OpenAPI::Controller::course', 'get_active_courses', 'eg_auth_token every_param.org') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeCourses','org','query','integer') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeCourses','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('activeRoles', '/courses/public_role_users', 'get', 'Retrieve all public roles used for courses', 'OpenILS::OpenAPI::Controller::course', 'get_all_course_public_roles', 'eg_auth_token every_param.org') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeRoles','org','query','integer') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeRoles','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourse', '/course/:id', 'get', 'Retrieve one detailed course', 'OpenILS::OpenAPI::Controller::course', 'get_course_detail', 'eg_auth_token param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourse','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveCourse','acmc') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourseMaterials', '/course/:id/materials', 'get', 'Retrieve detailed materials for one course', 'OpenILS::OpenAPI::Controller::course', 'get_course_materials', 'param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseMaterials','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseMaterials','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourseUsers', '/course/:id/public_role_users', 'get', 'Retrieve detailed user list for one course', 'open-ils.courses', 'open-ils.courses.course_users.retrieve', 'param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseUsers','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseUsers','array','object') ON CONFLICT DO NOTHING; + +--------- put likely stock endpoints into sets -------- +INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) + SELECT e.operation_id, s.name FROM openapi.endpoint e JOIN openapi.endpoint_set s ON (e.path LIKE '/'||RTRIM(s.name,'s')||'%') +ON CONFLICT DO NOTHING; + +-- Global Fieldmapper property-filtering org and user setting types +INSERT INTO config.settings_group (name,label) VALUES ('openapi','OpenAPI data access control'); + +INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi'); +INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi'); +UPDATE config.org_unit_setting_type + SET update_perm = (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_OPENAPI' LIMIT 1) + WHERE name IN ('REST.api.blacklist_properties','REST.api.whitelist_properties'); + +INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi'); +INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi'); + diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql index 8a64bfc30e..4109fbe7b7 100644 --- a/Open-ILS/src/sql/Pg/999.functions.global.sql +++ b/Open-ILS/src/sql/Pg/999.functions.global.sql @@ -14,6 +14,58 @@ * */ +CREATE OR REPLACE FUNCTION actor.verify_passwd(pw_usr integer, pw_type text, test_passwd text) RETURNS boolean AS $f$ +DECLARE + pw_salt TEXT; + api_enabled BOOL; +BEGIN + /* Returns TRUE if the password provided matches the in-db password. + * If the password type is salted, we compare the output of CRYPT(). + * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy + * 'main' passwords. + * + * Password type 'api' requires that the user be enabled as an + * integrator in the openapi.integrator table. + */ + + IF pw_type = 'api' THEN + SELECT enabled INTO api_enabled + FROM openapi.integrator + WHERE id = pw_usr; + + IF NOT FOUND OR api_enabled IS FALSE THEN + -- API integrator account not registered + RETURN FALSE; + END IF; + END IF; + + SELECT INTO pw_salt salt FROM actor.passwd + WHERE usr = pw_usr AND passwd_type = pw_type; + + IF NOT FOUND THEN + -- no such password + RETURN FALSE; + END IF; + + IF pw_salt IS NULL THEN + -- Password is unsalted, compare the un-CRYPT'ed values. + RETURN EXISTS ( + SELECT TRUE FROM actor.passwd WHERE + usr = pw_usr AND + passwd_type = pw_type AND + passwd = test_passwd + ); + END IF; + + RETURN EXISTS ( + SELECT TRUE FROM actor.passwd WHERE + usr = pw_usr AND + passwd_type = pw_type AND + passwd = CRYPT(test_passwd, pw_salt) + ); +END; +$f$ STRICT LANGUAGE plpgsql; + CREATE OR REPLACE FUNCTION actor.usr_merge_rows( table_name TEXT, col_name TEXT, src_usr INT, dest_usr INT ) RETURNS VOID AS $$ DECLARE sel TEXT; diff --git a/Open-ILS/src/sql/Pg/sql_file_manifest b/Open-ILS/src/sql/Pg/sql_file_manifest index 1f6d6c3be5..f94ceb458e 100644 --- a/Open-ILS/src/sql/Pg/sql_file_manifest +++ b/Open-ILS/src/sql/Pg/sql_file_manifest @@ -50,6 +50,7 @@ FTS_CONFIG_FILE 500.view.cross-schema.sql 600.schema.oai.sql +610.schema.openapi.sql 800.fkeys.sql diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql new file mode 100755 index 0000000000..07afff5149 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.openapi.sql @@ -0,0 +1,1470 @@ +BEGIN; + +-- Necessary pre-seed data + + +INSERT INTO actor.passwd_type (code, name, login, crypt_algo, iter_count) + VALUES ('api', 'OpenAPI Integration Password', TRUE, 'bf', 10) +ON CONFLICT DO NOTHING; + +-- Move top-level perms "down" ... +INSERT INTO permission.grp_perm_map (grp,perm,depth,grantable) + SELECT DISTINCT g.id, p.perm, p.depth, p.grantable + FROM permission.grp_perm_map p, + permission.grp_tree g + WHERE g.parent = 1 AND p.grp = 1; + +-- ... then remove the User version ... +DELETE FROM permission.grp_perm_map WHERE grp = 1; + +-- ... and add a new branch to the group tree for API perms +INSERT INTO permission.grp_tree (name, parent, description, application_perm) + VALUES ('API Integrator', 1, 'API Integration Accounts', 'group_application.api_integrator'); + +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Patron API', id, 'Patron API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Org Unit API', id, 'Org Unit API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Bib Record API', id, 'Bib Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Item Record API', id, 'Item Record API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Holds API', id, 'Holds API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Debt Collection API', id, 'Debt Collection API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; +INSERT INTO permission.grp_tree (name, parent, description, application_perm) SELECT 'Course Reserves API', id, 'Course Reserves API', 'group_application.api_integrator' FROM permission.grp_tree WHERE name = 'API Integrator'; + +INSERT INTO permission.perm_list (code) VALUES + ('group_application.api_integrator'), + ('API_LOGIN'), + ('REST.api'), + ('REST.api.patrons'), + ('REST.api.orgs'), + ('REST.api.bibs'), + ('REST.api.items'), + ('REST.api.holds'), + ('REST.api.collections'), + ('REST.api.courses') + --- ... etc +ON CONFLICT DO NOTHING; + +INSERT INTO permission.grp_perm_map (grp,perm,depth) + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='API Integrator' AND p.code IN ('API_LOGIN','REST.api') + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Patron API' AND p.code = 'REST.api.patrons' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Org Unit API' AND p.code = 'REST.api.orgs' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Bib Record API' AND p.code = 'REST.api.bibs' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Item Record API' AND p.code = 'REST.api.items' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Holds API' AND p.code = 'REST.api.holds' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Debt Collection API' AND p.code = 'REST.api.collections' + UNION + SELECT g.id, p.id, 0 FROM permission.grp_tree g, permission.perm_list p WHERE g.name='Course Reserves API' AND p.code = 'REST.api.courses' +ON CONFLICT DO NOTHING; + +DROP SCHEMA IF EXISTS openapi CASCADE; +CREATE SCHEMA IF NOT EXISTS openapi; + +CREATE TABLE IF NOT EXISTS openapi.integrator ( + id INT PRIMARY KEY REFERENCES actor.usr (id), + enabled BOOL NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS openapi.json_schema_datatype ( + name TEXT PRIMARY KEY, + label TEXT NOT NULL UNIQUE, + description TEXT +); +INSERT INTO openapi.json_schema_datatype (name,label) VALUES + ('boolean','Boolean'), + ('string','String'), + ('integer','Integer'), + ('number','Number'), + ('array','Array'), + ('object','Object') +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.json_schema_format ( + name TEXT PRIMARY KEY, + label TEXT NOT NULL UNIQUE, + description TEXT +); +INSERT INTO openapi.json_schema_format (name,label) VALUES + ('date-time','Timestamp'), + ('date','Date'), + ('time','Time'), + ('interval','Interval'), + ('email','Email Address'), + ('uri','URI'), + ('identifier','Opaque Identifier'), + ('money','Money'), + ('float','Floating Point Number'), + ('int64','Large Integer'), + ('password','Password') +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.rate_limit_definition ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, -- i18n + limit_interval INTERVAL NOT NULL, + limit_count INT NOT NULL +); +SELECT SETVAL('openapi.rate_limit_definition_id_seq'::TEXT, 100); +INSERT INTO openapi.rate_limit_definition (id, name, limit_interval, limit_count) VALUES + (1, 'Once per second', '1 second', 1), + (2, 'Ten per minute', '1 minute', 10), + (3, 'One hunderd per hour', '1 hour', 100), + (4, 'One thousand per hour', '1 hour', 1000), + (5, 'One thousand per 24 hour period', '24 hours', 1000), + (6, 'Ten thousand per 24 hour period', '24 hours', 10000), + (7, 'Unlimited', '1 second', 1000000), + (8, 'One hundred per second', '1 second', 100) +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.endpoint ( + operation_id TEXT PRIMARY KEY, + path TEXT NOT NULL, + http_method TEXT NOT NULL CHECK (http_method IN ('get','put','post','delete','patch')), + security TEXT NOT NULL DEFAULT 'bearerAuth' CHECK (security IN ('bearerAuth','basicAuth','cookieAuth','paramAuth')), + summary TEXT NOT NULL, + method_source TEXT NOT NULL, -- perl module or opensrf application, tested by regex and assumes opensrf app name contains a "." + method_name TEXT NOT NULL, + method_params TEXT, -- eg, 'eg_auth_token hold' or 'eg_auth_token eg_user_id circ req.json' + active BOOL NOT NULL DEFAULT TRUE, + rate_limit INT REFERENCES openapi.rate_limit_definition (id), + CONSTRAINT path_and_method_once UNIQUE (path, http_method) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_param ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + name TEXT NOT NULL CHECK (name ~ '^\w+$'), + required BOOL NOT NULL DEFAULT FALSE, + in_part TEXT NOT NULL DEFAULT 'query' CHECK (in_part IN ('path','query','header','cookie')), + fm_type TEXT, + schema_type TEXT REFERENCES openapi.json_schema_datatype (name), + schema_format TEXT REFERENCES openapi.json_schema_format (name), + array_items TEXT REFERENCES openapi.json_schema_datatype (name), + default_value TEXT, + CONSTRAINT endpoint_and_name_once UNIQUE (endpoint, name), + CONSTRAINT format_requires_type CHECK (schema_format IS NULL OR schema_type IS NOT NULL), + CONSTRAINT array_items_requires_array_type CHECK (array_items IS NULL OR schema_type = 'array') +); +CREATE TABLE IF NOT EXISTS openapi.endpoint_response ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + validate BOOL NOT NULL DEFAULT TRUE, + status INT NOT NULL DEFAULT 200, + content_type TEXT NOT NULL DEFAULT 'application/json', + description TEXT NOT NULL DEFAULT 'Success', + fm_type TEXT, + schema_type TEXT REFERENCES openapi.json_schema_datatype (name), + schema_format TEXT REFERENCES openapi.json_schema_format (name), + array_items TEXT REFERENCES openapi.json_schema_datatype (name), + CONSTRAINT endpoint_status_content_type_once UNIQUE (endpoint, status, content_type) +); + +CREATE TABLE IF NOT EXISTS openapi.perm_set ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); -- push sequence value +SELECT SETVAL('openapi.perm_set_id_seq'::TEXT, 1001); +INSERT INTO openapi.perm_set (id, name) VALUES + (1,'Self - API only'), + (2,'Patrons - API only'), + (3,'Orgs - API only'), + (4,'Bibs - API only'), + (5,'Items - API only'), + (6,'Holds - API only'), + (7,'Collections - API only'), + (8,'Courses - API only'), + + (101,'Self - standard permissions'), + (102,'Patrons - standard permissions'), + (103,'Orgs - standard permissions'), + (104,'Bibs - standard permissions'), + (105,'Items - standard permissions'), + (106,'Holds - standard permissions'), + (107,'Collections - standard permissions'), + (108,'Courses - standard permissions') +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.perm_set_perm_map ( + id SERIAL PRIMARY KEY, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); +INSERT INTO openapi.perm_set_perm_map (perm_set, perm) + SELECT 1, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api') + UNION + SELECT 2, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.patrons') + UNION + SELECT 3, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.orgs') + UNION + SELECT 4, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.bibs') + UNION + SELECT 5, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.items') + UNION + SELECT 6, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.holds') + UNION + SELECT 7, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.collections') + UNION + SELECT 8, id FROM permission.perm_list WHERE code IN ('API_LOGIN','REST.api','REST.api.cources') + UNION + -- ... + SELECT 101, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 102, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 103, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 104, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 105, id FROM permission.perm_list WHERE code IN ('OPAC_LOGIN') + UNION + SELECT 106, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 107, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN','VIEW_USER') + UNION + SELECT 108, id FROM permission.perm_list WHERE code IN ('STAFF_LOGIN') + +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_set_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint(operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set ( + name TEXT PRIMARY KEY, + description TEXT NOT NULL, + active BOOL NOT NULL DEFAULT TRUE, + rate_limit INT REFERENCES openapi.rate_limit_definition (id) +); +INSERT INTO openapi.endpoint_set (name, description) VALUES + ('self', 'Methods for retrieving and manipulating your own user account information'), + ('orgs', 'Methods for retrieving and manipulating organizational unit information'), + ('patrons', 'Methods for retrieving and manipulating patron information'), + ('holds', 'Methods for accessing and manipulating hold data'), + ('collections', 'Methods for accessing and manipulating patron debt collections data'), + ('bibs', 'Methods for accessing and manipulating bibliographic records and related data'), + ('items', 'Methods for accessing and manipulating barcoded item records'), + ('courses', 'Methods for accessing and manipulating course reserve data') +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.endpoint_user_rate_limit_map ( + id SERIAL PRIMARY KEY, + accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_accessor_once UNIQUE (accessor, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_user_rate_limit_map ( + id SERIAL PRIMARY KEY, + accessor INT NOT NULL REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_accessor_once UNIQUE (accessor, endpoint_set) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_ip_rate_limit_map ( + id SERIAL PRIMARY KEY, + ip_range INET NOT NULL, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_ip_range_once UNIQUE (ip_range, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_ip_rate_limit_map ( + id SERIAL PRIMARY KEY, + ip_range INET NOT NULL, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + rate_limit INT NOT NULL REFERENCES openapi.rate_limit_definition (id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_ip_range_once UNIQUE (ip_range, endpoint_set) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_endpoint_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT endpoint_set_endpoint_once UNIQUE (endpoint_set, endpoint) +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_perm_map ( + id SERIAL PRIMARY KEY, + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_set_map ( + id SERIAL PRIMARY KEY, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + perm_set INT NOT NULL REFERENCES openapi.perm_set (id) ON UPDATE CASCADE ON DELETE CASCADE +); +INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES + ('self', 1), ('self', 101), + ('patrons', 2), ('patrons', 102), + ('orgs', 3), ('orgs', 103), + ('bibs', 4), ('bibs', 104), + ('items', 5), ('items', 105), + ('holds', 6), ('holds', 106), + ('collections', 7), ('collections', 107), + ('courses', 8), ('courses', 108) +ON CONFLICT DO NOTHING; + +CREATE TABLE IF NOT EXISTS openapi.endpoint_set_perm_map ( + id SERIAL PRIMARY KEY, + endpoint_set TEXT NOT NULL REFERENCES openapi.endpoint_set (name) ON UPDATE CASCADE ON DELETE CASCADE, + perm INT NOT NULL REFERENCES permission.perm_list (id) ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS openapi.authen_attempt_log ( + request_id TEXT PRIMARY KEY, + attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ip_addr INET, + cred_user TEXT, + token TEXT +); +CREATE INDEX authen_cred_user_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, cred_user); +CREATE INDEX authen_ip_addr_attempt_time_idx ON openapi.authen_attempt_log (attempt_time, ip_addr); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_access_attempt_log ( + request_id TEXT PRIMARY KEY, + attempt_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + endpoint TEXT NOT NULL REFERENCES openapi.endpoint (operation_id) ON UPDATE CASCADE ON DELETE CASCADE, + allowed BOOL NOT NULL, + ip_addr INET, + accessor INT REFERENCES actor.usr (id) ON UPDATE CASCADE ON DELETE CASCADE, + token TEXT +); +CREATE INDEX access_accessor_attempt_time_idx ON openapi.endpoint_access_attempt_log (accessor, attempt_time); + +CREATE TABLE IF NOT EXISTS openapi.endpoint_dispatch_log ( + request_id TEXT PRIMARY KEY, + complete_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), + error BOOL NOT NULL +); + +CREATE OR REPLACE FUNCTION actor.verify_passwd(pw_usr integer, pw_type text, test_passwd text) RETURNS boolean AS $f$ +DECLARE + pw_salt TEXT; + api_enabled BOOL; +BEGIN + /* Returns TRUE if the password provided matches the in-db password. + * If the password type is salted, we compare the output of CRYPT(). + * NOTE: test_passwd is MD5(salt || MD5(password)) for legacy + * 'main' passwords. + * + * Password type 'api' requires that the user be enabled as an + * integrator in the openapi.integrator table. + */ + + IF pw_type = 'api' THEN + SELECT enabled INTO api_enabled + FROM openapi.integrator + WHERE id = pw_usr; + + IF NOT FOUND OR api_enabled IS FALSE THEN + -- API integrator account not registered + RETURN FALSE; + END IF; + END IF; + + SELECT INTO pw_salt salt FROM actor.passwd + WHERE usr = pw_usr AND passwd_type = pw_type; + + IF NOT FOUND THEN + -- no such password + RETURN FALSE; + END IF; + + IF pw_salt IS NULL THEN + -- Password is unsalted, compare the un-CRYPT'ed values. + RETURN EXISTS ( + SELECT TRUE FROM actor.passwd WHERE + usr = pw_usr AND + passwd_type = pw_type AND + passwd = test_passwd + ); + END IF; + + RETURN EXISTS ( + SELECT TRUE FROM actor.passwd WHERE + usr = pw_usr AND + passwd_type = pw_type AND + passwd = CRYPT(test_passwd, pw_salt) + ); +END; +$f$ STRICT LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION openapi.find_default_endpoint_rate_limit (target_endpoint TEXT) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + -- Default rate limits can be applied at the endpoint or endpoint_set level; + -- endpoint overrides endpoint_set, and we choose the most restrictive from + -- the set if we have to look there. + SELECT d.* INTO def_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint e ON (e.rate_limit = d.id) + WHERE e.operation_id = target_endpoint; + + IF NOT FOUND THEN + SELECT d.* INTO def_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set es ON (es.rate_limit = d.id) + JOIN openapi.endpoint_set_endpoint_map m ON (es.name = m.endpoint_set AND m.endpoint = target_endpoint) + -- This ORDER BY calculates the avg time between requests the user would have to wait to perfectly + -- avoid rate limiting. So, a bigger wait means it's more restrictive. We take the most restrictive + -- set-applied one. + ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + -- If there's no default for the endpoint or set, we provide 1/sec. + IF NOT FOUND THEN + def_rl.limit_interval := '1 second'::INTERVAL; + def_rl.limit_count := 1; + END IF; + + RETURN def_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.find_user_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_u_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + SELECT d.* INTO def_u_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_user_rate_limit_map e ON (e.rate_limit = d.id) + WHERE e.endpoint = target_endpoint + AND e.accessor = accessing_usr; + + IF NOT FOUND THEN + SELECT d.* INTO def_u_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set_user_rate_limit_map e ON (e.rate_limit = d.id AND e.accessor = accessing_usr) + JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint) + ORDER BY EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + RETURN def_u_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.find_ip_addr_endpoint_rate_limit (target_endpoint TEXT, from_ip_addr INET) RETURNS openapi.rate_limit_definition AS $f$ +DECLARE + def_i_rl openapi.rate_limit_definition%ROWTYPE; +BEGIN + SELECT d.* INTO def_i_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_ip_rate_limit_map e ON (e.rate_limit = d.id) + WHERE e.endpoint = target_endpoint + AND e.ip_range && from_ip_addr + -- For IPs, we order first by the size of the ranges that we + -- matched (mask length), most specific (smallest block of IPs) + -- first, then by the restrictiveness of the limit, more restrictive first. + ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + + IF NOT FOUND THEN + SELECT d.* INTO def_i_rl + FROM openapi.rate_limit_definition d + JOIN openapi.endpoint_set_ip_rate_limit_map e ON (e.rate_limit = d.id AND ip_range && from_ip_addr) + JOIN openapi.endpoint_set_endpoint_map m ON (e.endpoint_set = m.endpoint_set AND m.endpoint = target_endpoint) + ORDER BY MASKLEN(e.ip_range) DESC, EXTRACT(EPOCH FROM d.limit_interval) / d.limit_count::NUMERIC DESC + LIMIT 1; + END IF; + + RETURN def_i_rl; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.check_generic_endpoint_rate_limit (target_endpoint TEXT, accessing_usr INT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; + def_u_rl openapi.rate_limit_definition%ROWTYPE; + def_i_rl openapi.rate_limit_definition%ROWTYPE; + u_wait INT; + i_wait INT; +BEGIN + def_rl := openapi.find_default_endpoint_rate_limit(target_endpoint); + + IF accessing_usr IS NOT NULL THEN + def_u_rl := openapi.find_user_endpoint_rate_limit(target_endpoint, accessing_usr); + END IF; + + IF from_ip_addr IS NOT NULL THEN + def_i_rl := openapi.find_ip_addr_endpoint_rate_limit(target_endpoint, from_ip_addr); + END IF; + + -- Now we test the user-based and IP-based limits in their focused way... + IF def_u_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.accessor = accessing_usr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + IF def_i_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + -- If there are no user-specific or IP-based limit + -- overrides; check endpoint-wide limits for user, + -- then IP, and if we were passed neither, then limit + -- endpoint access for all users. Better to lock it + -- all down than to set the servers on fire. + IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN + IF accessing_usr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.accessor ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.accessor = accessing_usr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSIF from_ip_addr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSE -- we have no user and no IP, global per-endpoint rate limit? + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.endpoint ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.endpoint_access_attempt_log l + WHERE l.endpoint = target_endpoint + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + END IF; + END IF; + + -- Send back the largest required wait time, or NULL for no restriction + u_wait := GREATEST(u_wait,i_wait); + IF u_wait > 0 THEN + RETURN u_wait; + END IF; + + RETURN NULL; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION openapi.check_auth_endpoint_rate_limit (accessing_usr TEXT DEFAULT NULL, from_ip_addr INET DEFAULT NULL) RETURNS INT AS $f$ +DECLARE + def_rl openapi.rate_limit_definition%ROWTYPE; + def_u_rl openapi.rate_limit_definition%ROWTYPE; + def_i_rl openapi.rate_limit_definition%ROWTYPE; + u_wait INT; + i_wait INT; +BEGIN + def_rl := openapi.find_default_endpoint_rate_limit('authenticateUser'); + + IF accessing_usr IS NOT NULL THEN + SELECT (openapi.find_user_endpoint_rate_limit('authenticateUser', u.id)).* INTO def_u_rl + FROM actor.usr u + WHERE u.usrname = accessing_usr; + END IF; + + IF from_ip_addr IS NOT NULL THEN + def_i_rl := openapi.find_ip_addr_endpoint_rate_limit('authenticateUser', from_ip_addr); + END IF; + + -- Now we test the user-based and IP-based limits in their focused way... + IF def_u_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.cred_user = accessing_usr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + IF def_i_rl.id IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_u_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_u_rl.limit_interval + ) x + WHERE running_count = def_u_rl.limit_count; + END IF; + + -- If there are no user-specific or IP-based limit + -- overrides; check endpoint-wide limits for user, + -- then IP, and if we were passed neither, then limit + -- endpoint access for all users. Better to lock it + -- all down than to set the servers on fire. + IF COALESCE(u_wait, i_wait) IS NULL AND COALESCE(def_i_rl.id, def_u_rl.id) IS NULL THEN + IF accessing_usr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.cred_user ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.cred_user = accessing_usr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSIF from_ip_addr IS NOT NULL THEN + SELECT CEIL(EXTRACT(EPOCH FROM (x.attempt_time + def_rl.limit_interval) - NOW())) INTO i_wait + FROM (SELECT l.attempt_time, + COUNT(*) OVER (PARTITION BY l.ip_addr ORDER BY l.attempt_time DESC) AS running_count + FROM openapi.authen_attempt_log l + WHERE l.ip_addr = from_ip_addr + AND l.attempt_time > NOW() - def_rl.limit_interval + ) x + WHERE running_count = def_rl.limit_count; + ELSE -- we have no user and no IP, global auth attempt rate limit? + SELECT CEIL(EXTRACT(EPOCH FROM (l.attempt_time + def_rl.limit_interval) - NOW())) INTO u_wait + FROM openapi.authen_attempt_log l + WHERE l.attempt_time > NOW() - def_rl.limit_interval + ORDER BY l.attempt_time DESC + LIMIT 1 OFFSET def_rl.limit_count; + END IF; + END IF; + + -- Send back the largest required wait time, or NULL for no restriction + u_wait := GREATEST(u_wait,i_wait); + IF u_wait > 0 THEN + RETURN u_wait; + END IF; + + RETURN NULL; +END; +$f$ STABLE LANGUAGE PLPGSQL; + +--===================================== Seed data ================================== + +-- ===== authentication +INSERT INTO openapi.endpoint (operation_id, path, security, http_method, summary, method_source, method_name, method_params, rate_limit) VALUES +-- Builtin auth-related methods, give all users and IPs 100/s of each + ('authenticateUser', '/self/auth', 'basicAuth', 'get', 'Authenticate API user', 'OpenILS::OpenAPI::Controller', 'authenticateUser', 'param.u param.p param.t', 8), + ('logoutUser', '/self/auth', 'bearerAuth', 'delete', 'Logout API user', 'open-ils.auth', 'open-ils.auth.session.delete', 'eg_auth_token', 8) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('authenticateUser','u','query','string',NULL,NULL), + ('authenticateUser','p','query','string','password',NULL), + ('authenticateUser','t','query','string',NULL,'api') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('authenticateUser','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('authenticateUser','text/plain') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('logoutUser','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('logoutUser','text/plain') ON CONFLICT DO NOTHING; + +-- ===== self-service +-- get/update me +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfProfile', '/self/me', 'get', 'Return patron/user record for logged in user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token eg_user_id'), + ('selfUpdateParts', '/self/me', 'patch', 'Update portions of the logged in user''s record', 'OpenILS::OpenAPI::Controller::patron', 'update_user_parts', 'eg_auth_token req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveSelfProfile','au') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('selfUpdateParts','object') ON CONFLICT DO NOTHING; + +-- get my standing penalties +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'selfActivePenalties', + '/self/standing_penalties', + 'get', + 'Produces patron-visible penalty details for the authorized account', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token eg_user_id "1"' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('selfActivePenalties','array','object') ON CONFLICT DO NOTHING; -- array of fleshed ausp + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'selfPenalty', + '/self/standing_penalty/:penaltyid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token eg_user_id "1" param.penaltyid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('selfPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING; + + + +-- manage my holds +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfHolds', '/self/holds', 'get', 'Return unfilled holds for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token eg_user_id'), + ('requestSelfHold', '/self/holds', 'post', 'Request a hold for the authorized account', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token eg_user_id req.json'), + ('retrieveSelfHold', '/self/hold/:hold', 'get', 'Retrieve one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token eg_user_id param.hold'), + ('updateSelfHold', '/self/hold/:hold', 'patch', 'Update one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token eg_user_id param.hold req.json'), + ('cancelSelfHold', '/self/hold/:hold', 'delete', 'Cancel one hold for the logged in user', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token eg_user_id param.hold "6"') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrieveSelfHold','hold','path','integer',TRUE), + ('updateSelfHold','hold','path','integer',TRUE), + ('cancelSelfHold','hold','path','integer',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfHolds','array','object') ON CONFLICT DO NOTHING; + + +-- general xact list +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfXacts', + '/self/transactions/:state', + 'get', + 'Produces a list of transactions of the logged in user', + 'OpenILS::OpenAPI::Controller::patron', + 'transactions_by_state', + 'eg_auth_token eg_user_id state param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrieveSelfXacts','state','path','string',NULL,NULL,TRUE), + ('retrieveSelfXacts','limit','query','integer',NULL,NULL,FALSE), + ('retrieveSelfXacts','offset','query','integer',NULL,'0',FALSE), + ('retrieveSelfXacts','sort','query','string',NULL,'desc',FALSE), + ('retrieveSelfXacts','before','query','string','date-time',NULL,FALSE), + ('retrieveSelfXacts','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfXacts','array','object') ON CONFLICT DO NOTHING; + +-- general xact detail +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfXact', + '/self/transaction/:id', + 'get', + 'Details of one transaction for the logged in user', + 'open-ils.actor', + 'open-ils.actor.user.transaction.fleshed.retrieve', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveSelfXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING; + + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfCircs', '/self/checkouts', 'get', 'Open Circs for the logged in user', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token eg_user_id'), + ('requestSelfCirc', '/self/checkouts', 'post', 'Attempt a circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token eg_user_id req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircs','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveSelfCircHistory', + '/self/checkouts/history', + 'get', + 'Historical Circs for logged in user', + 'OpenILS::OpenAPI::Controller::patron', + 'circulation_history', + 'eg_auth_token eg_user_id param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('retrieveSelfCircHistory','limit','query','integer',NULL,NULL), + ('retrieveSelfCircHistory','offset','query','integer',NULL,'0'), + ('retrieveSelfCircHistory','sort','query','string',NULL,'desc'), + ('retrieveSelfCircHistory','before','query','string','date-time',NULL), + ('retrieveSelfCircHistory','after','query','string','date-time',NULL) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveSelfCircHistory','array','object') ON CONFLICT DO NOTHING; + + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveSelfCirc', '/self/checkout/:id', 'get', 'Retrieve one circulation for the logged in user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'), + ('renewSelfCirc', '/self/checkout/:id', 'put', 'Renew one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id eg_user_id'), + ('checkinSelfCirc', '/self/checkout/:id', 'delete', 'Check in one circulation for the logged in user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id eg_user_id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrieveSelfCirc','id','path','integer',TRUE), + ('renewSelfCirc','id','path','integer',TRUE), + ('checkinSelfCirc','id','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +-- bib, item, and org methods +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOrgList', + '/org_units', + 'get', + 'List of org units', + 'OpenILS::OpenAPI::Controller::org', + 'flat_org_list', + 'every_param.field every_param.comparison every_param.value' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES + ('retrieveOrgList','field','query','string'), + ('retrieveOrgList','comparison','query','string'), + ('retrieveOrgList','value','query','string') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOrgList','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneOrg', + '/org_unit/:id', + 'get', + 'One org unit', + 'OpenILS::OpenAPI::Controller::org', + 'one_org', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneOrg','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneOrg','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name) VALUES ( + 'retrieveFullOrgTree', + '/org_tree', + 'get', + 'Full hierarchical tree of org units', + 'OpenILS::OpenAPI::Controller::org', + 'full_tree' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveFullOrgTree','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePartialOrgTree', + '/org_tree/:id', + 'get', + 'Partial hierarchical tree of org units starting from a specific org unit', + 'OpenILS::OpenAPI::Controller::org', + 'one_tree', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePartialOrgTree','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePartialOrgTree','aou') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'createOneBib', + '/bibs', + 'post', + 'Attempts to create a bibliographic record using MARCXML passed as the request content', + 'open-ils.cat', + 'open-ils.cat.biblio.record.xml.create', + 'eg_auth_token req.text param.sourcename' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('createOneBib','sourcename','query','string') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('createOneBib','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateOneBib', + '/bib/:id', + 'put', + 'Attempts to update a bibliographic record using MARCXML passed as the request content', + 'open-ils.cat', + 'open-ils.cat.biblio.record.marc.replace', + 'eg_auth_token param.id req.text param.sourcename' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('updateOneBib','sourcename','query','string') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('updateOneBib','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'deleteOneBib', + '/bib/:id', + 'delete', + 'Attempts to delete a bibliographic record', + 'open-ils.cat', + 'open-ils.cat.biblio.record_entry.delete', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,validate) VALUES ('deleteOneBib','integer',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateBREParts', + '/bib/:id', + 'patch', + 'Attempts to update the biblio.record_entry metadata surrounding a bib record', + 'OpenILS::OpenAPI::Controller::bib', + 'update_bre_parts', + 'eg_auth_token param.id req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateBREParts','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateBREParts','bre') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneBib', + '/bib/:id', + 'get', + 'Retrieve a bibliographic record, either full biblio::record_entry object, or just the MARCXML', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveOneBib','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveOneBib','bre') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/xml') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,content_type) VALUES ('retrieveOneBib','application/octet-stream') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveOneBibHoldings', + '/bib/:id/holdings', + 'get', + 'Retrieve the holdings data for a bibliographic record', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib_holdings', + 'param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value,required) VALUES + ('retrieveOneBibHoldings','id','path','integer',NULL,TRUE), + ('retrieveOneBibHoldings','limit','query','integer',NULL,FALSE), + ('retrieveOneBibHoldings','offset','query','integer','0',FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveOneBibHoldings','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'bibDisplayFields', + '/bib/:id/display_fields', + 'get', + 'Retrieve display-related data for a bibliographic record', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_one_bib_display_fields', + 'param.id req.text' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('bibDisplayFields','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('bibDisplayFields','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'newItems', + '/items/fresh', + 'get', + 'Retrieve a list of newly added items', + 'OpenILS::OpenAPI::Controller::bib', + 'fetch_new_items', + 'param.limit param.offset param.maxage' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','limit','query','integer','0') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,default_value) VALUES ('newItems','offset','query','integer','100') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format) VALUES ('newItems','maxage','query','string','interval') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('newItems','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrieveItem', + '/item/:barcode', + 'get', + 'Retrieve one item by its barcode', + 'OpenILS::OpenAPI::Controller::bib', + 'item_by_barcode', + 'param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('retrieveItem','Item Lookup Failed','404','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'createItem', + '/items', + 'post', + 'Create an item record', + 'OpenILS::OpenAPI::Controller::bib', + 'create_or_update_one_item', + 'eg_auth_token req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('createItem','Item Creation Failed','400') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('createItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'updateItem', + '/item/:barcode', + 'patch', + 'Update a restricted set of item record fields', + 'OpenILS::OpenAPI::Controller::bib', + 'create_or_update_one_item', + 'eg_auth_token req.json param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('updateItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status) VALUES ('updateItem','Item Update Failed','400') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('updateItem','acp') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'deleteItem', + '/item/:barcode', + 'delete', + 'Delete one item record', + 'OpenILS::OpenAPI::Controller::bib', + 'delete_one_item', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('deleteItem','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,description,status,schema_type) VALUES ('deleteItem','Item Deletion Failed','404','object') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('deleteItem','boolean') ON CONFLICT DO NOTHING; + + +-- === patron (non-self) methods +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'searchPatrons', + '/patrons', + 'get', + 'List of patrons matching requested conditions', + 'OpenILS::OpenAPI::Controller::patron', + 'find_users', + 'eg_auth_token every_param.field every_param.comparison every_param.value param.limit param.offset' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES + ('searchPatrons','field','query','string'), + ('searchPatrons','comparison','query','string'), + ('searchPatrons','value','query','string') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value) VALUES + ('searchPatrons','offset','query','integer',NULL,'0'), + ('searchPatrons','limit','query','integer',NULL,'100') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('searchPatrons','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'verifyUserCredentials', + '/patrons/verify', + 'get', + 'Verify the credentials for a user account', + 'open-ils.actor', + 'open-ils.actor.verify_user_password', + 'eg_auth_token param.barcode param.usrname "" param.password' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('verifyUserCredentials','barcode','query','string',NULL,FALSE), + ('verifyUserCredentials','usrname','query','string',NULL,FALSE), + ('verifyUserCredentials','password','query','string','password',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('verifyUserCredentials','boolean') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronProfile', '/patron/:userid', 'get', 'Return patron/user record for the requested user', 'OpenILS::OpenAPI::Controller::patron', 'deliver_user', 'eg_auth_token param.userid') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronProfile','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrievePatronProfile','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronIdByCardBarcode', + '/patrons/by_barcode/:barcode/id', + 'get', + 'Retrieve patron id by barcode', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByCardBarcode','integer') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronIdByUsername', + '/patrons/by_username/:username/id', + 'get', + 'Retrieve patron id by username', + 'open-ils.actor', + 'open-ils.actor.user.retrieve_id_by_barcode_or_username', + 'eg_auth_token "" param.username' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronIdByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronIdByUsername','integer') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronByCardBarcode', + '/patrons/by_barcode/:barcode', + 'get', + 'Retrieve patron by barcode', + 'OpenILS::OpenAPI::Controller::patron', + 'user_by_identifier_string', + 'eg_auth_token param.barcode' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByCardBarcode','barcode','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByCardBarcode','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronByUsername', + '/patrons/by_username/:username', + 'get', + 'Retrieve patron by username', + 'OpenILS::OpenAPI::Controller::patron', + 'user_by_identifier_string', + 'eg_auth_token "" param.username' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronByUsername','username','path','string',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronByUsername','au') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronCircHistory', + '/patron/:userid/checkouts/history', + 'get', + 'Historical Circs for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'circulation_history', + 'eg_auth_token param.userid param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrievePatronCircHistory','userid','path','integer',NULL,NULL,TRUE), + ('retrievePatronCircHistory','limit','query','integer',NULL,NULL,FALSE), + ('retrievePatronCircHistory','offset','query','integer',NULL,'0',FALSE), + ('retrievePatronCircHistory','sort','query','string',NULL,'desc',FALSE), + ('retrievePatronCircHistory','before','query','string','date-time',NULL,FALSE), + ('retrievePatronCircHistory','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircHistory','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronHolds', '/patron/:userid/holds', 'get', 'Retrieve unfilled holds for a patron', 'OpenILS::OpenAPI::Controller::hold', 'open_holds', 'eg_auth_token param.userid'), + ('requestPatronHold', '/patron/:userid/holds', 'post', 'Request a hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'request_hold', 'eg_auth_token param.userid req.json'), + ('retrievePatronHold','/patron/:userid/hold/:hold', 'get', 'Retrieve one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'fetch_user_hold', 'eg_auth_token param.userid param.hold'), + ('updatePatronHold', '/patron/:userid/hold/:hold', 'patch', 'Update one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'update_user_hold', 'eg_auth_token param.userid param.hold req.json'), + ('cancelPatronHold', '/patron/:userid/hold/:hold', 'delete', 'Cancel one hold for a patron', 'OpenILS::OpenAPI::Controller::hold', 'cancel_user_hold', 'eg_auth_token param.userid param.hold "6"') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrievePatronHolds','userid','path','integer',TRUE), + ('retrievePatronHold','userid','path','integer',TRUE), + ('retrievePatronHold','hold','path','integer',TRUE), + ('requestPatronHold','userid','path','integer',TRUE), + ('updatePatronHold','userid','path','integer',TRUE), + ('updatePatronHold','hold','path','integer',TRUE), + ('cancelPatronHold','userid','path','integer',TRUE), + ('cancelPatronHold','hold','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveHoldPickupLocations', '/holds/pickup_locations', 'get', 'Retrieve all valid hold/reserve pickup locations', 'OpenILS::OpenAPI::Controller::hold', 'valid_hold_pickup_locations', '') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveHoldPickupLocations','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveHold', '/hold/:hold', 'get', 'Retrieve one hold object', 'open-ils.circ', 'open-ils.circ.hold.details.retrieve', 'eg_auth_token param.hold') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveHold','hold','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronXacts', + '/patron/:userid/transactions/:state', + 'get', + 'Produces a list of transactions of the specified user', + 'OpenILS::OpenAPI::Controller::patron', + 'transactions_by_state', + 'eg_auth_token param.userid state param.limit param.offset param.sort param.before param.after' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('retrievePatronXacts','userid','path','integer',NULL,NULL,TRUE), + ('retrievePatronXacts','state','path','string',NULL,NULL,TRUE), + ('retrievePatronXacts','limit','query','integer',NULL,NULL,FALSE), + ('retrievePatronXacts','offset','query','integer',NULL,'0',FALSE), + ('retrievePatronXacts','sort','query','string',NULL,'desc',FALSE), + ('retrievePatronXacts','before','query','string','date-time',NULL,FALSE), + ('retrievePatronXacts','after','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronXacts','array','object') ON CONFLICT DO NOTHING; + +-- general xact detail +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'retrievePatronXact', + '/patron/:userid/transaction/:id', + 'get', + 'Details of one transaction for the specified user', + 'open-ils.actor', + 'open-ils.actor.user.transaction.fleshed.retrieve', + 'eg_auth_token param.id' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronXact','id','path','integer',TRUE) ON CONFLICT DO NOTHING; + + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronCircs', '/patron/:userid/checkouts', 'get', 'Open Circs for a patron', 'open-ils.circ', 'open-ils.circ.actor.user.checked_out.atomic', 'eg_auth_token param.userid'), + ('requestPatronCirc', '/patron/:userid/checkouts', 'post', 'Attempt a circulation for a patron', 'OpenILS::OpenAPI::Controller::patron', 'checkout_item', 'eg_auth_token param.userid req.json') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrievePatronCircs','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('requestPatronCirc','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrievePatronCircs','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrievePatronCirc', '/patron/:userid/checkout/:id', 'get', 'Retrieve one circulation for the specified user', 'open-ils.actor', 'open-ils.actor.user.transaction.fleshed.retrieve', 'eg_auth_token param.id'), + ('renewPatronCirc', '/patron/:userid/checkout/:id', 'put', 'Renew one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'renew_circ', 'eg_auth_token param.id param.userid'), + ('checkinPatronCirc', '/patron/:userid/checkout/:id', 'delete', 'Check in one circulation for the specified user', 'OpenILS::OpenAPI::Controller::patron', 'checkin_circ', 'eg_auth_token param.id param.userid') +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('retrievePatronCirc','userid','path','integer',TRUE), + ('retrievePatronCirc','id','path','integer',TRUE), + ('renewPatronCirc','userid','path','integer',TRUE), + ('renewPatronCirc','id','path','integer',TRUE), + ('checkinPatronCirc','userid','path','integer',TRUE), + ('checkinPatronCirc','id','path','integer',TRUE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronATEvents', + '/patron/:userid/triggered_events', + 'get', + 'Retrieve a list of A/T events for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_at_events', + 'eg_auth_token param.userid param.limit param.offset param.before param.after every_param.hook' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('patronATEvents','userid','path','integer',NULL,NULL,TRUE), + ('patronATEvents','limit','query','integer',NULL,'100',FALSE), + ('patronATEvents','offset','query','integer',NULL,'0',FALSE), + ('patronATEvents','before','query','string','date-time',NULL,FALSE), + ('patronATEvents','after','query','string','date-time',NULL,FALSE), + ('patronATEvents','hook','query','string',NULL,NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronATEvents','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronATEvent', + '/patron/:userid/triggered_event/:eventid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_at_events', + 'eg_auth_token param.userid "1" "0" "" "" "" param.eventid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','eventid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronATEvent','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActivePenalties', + '/patron/:userid/standing_penalties', + 'get', + 'Retrieve all penalty details for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token param.userid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActivePenalties','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivePenalties','array','integer') ON CONFLICT DO NOTHING; -- array of ausp ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronPenalty', + '/patron/:userid/standing_penalty/:penaltyid', + 'get', + 'Retrieve one penalty for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'standing_penalties', + 'eg_auth_token param.userid "0" param.penaltyid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','penaltyid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronPenalty','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActiveMessages', + '/patron/:userid/messages', + 'get', + 'Retrieve all active message ids for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_messages', + 'eg_auth_token param.userid "0"' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronActiveMessages','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActiveMessages','array','integer') ON CONFLICT DO NOTHING; -- array of aum ids + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessage', + '/patron/:userid/message/:msgid', + 'get', + 'Retrieve one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_messages', + 'eg_auth_token param.userid "0" param.msgid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessage','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessage','aum') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessageUpdate', + '/patron/:userid/message/:msgid', + 'patch', + 'Update one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'update_usr_message', + 'eg_auth_token eg_user_id param.userid param.msgid req.json' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageUpdate','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('patronMessageUpdate','aum') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronMessageArchive', + '/patron/:userid/message/:msgid', + 'delete', + 'Archive one message for a patron', + 'OpenILS::OpenAPI::Controller::patron', + 'archive_usr_message', + 'eg_auth_token eg_user_id param.userid param.msgid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','msgid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('patronMessageArchive','userid','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type) VALUES ('patronMessageArchive','boolean') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'patronActivityLog', + '/patron/:userid/activity', + 'get', + 'Retrieve patron activity (authen, authz, etc)', + 'OpenILS::OpenAPI::Controller::patron', + 'usr_activity', + 'eg_auth_token param.userid param.maxage param.limit param.offset param.sort' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,default_value,required) VALUES + ('patronActivityLog','userid','path','integer',NULL,NULL,TRUE), + ('patronActivityLog','limit','query','integer',NULL,'100',FALSE), + ('patronActivityLog','offset','query','integer',NULL,'0',FALSE), + ('patronActivityLog','sort','query','string',NULL,'desc',FALSE), + ('patronActivityLog','maxage','query','string','date-time',NULL,FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('patronActivityLog','array','object') ON CONFLICT DO NOTHING; + +------- collections +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPatronsOfInterest', + '/collections/:shortname/users_of_interest', + 'get', + 'List of patrons to consider for collections based on the search criteria provided.', + 'open-ils.collections', + 'open-ils.collections.users_of_interest.retrieve', + 'eg_auth_token param.fine_age param.fine_amount param.shortname' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPatronsOfInterest','shortname','path','string',NULL,TRUE), + ('collectionsPatronsOfInterest','fine_age','query','integer',NULL,TRUE), + ('collectionsPatronsOfInterest','fine_amount','query','string','money',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterest','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPatronsOfInterestWarning', + '/collections/:shortname/users_of_interest/warning', + 'get', + 'List of patrons with the PATRON_EXCEEDS_COLLECTIONS_WARNING penalty to consider for collections based on the search criteria provided.', + 'open-ils.collections', + 'open-ils.collections.users_of_interest.warning_penalty.retrieve', + 'eg_auth_token param.shortname param.min_age param.max_age' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPatronsOfInterestWarning','shortname','path','string',NULL,TRUE), + ('collectionsPatronsOfInterestWarning','min_age','query','string','date-time',FALSE), + ('collectionsPatronsOfInterestWarning','max_age','query','string','date-time',FALSE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsPatronsOfInterestWarning','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsGetPatronDetail', + '/patron/:usrid/collections/:shortname', + 'get', + 'Get collections-related transaction details for a patron.', + 'open-ils.collections', + 'open-ils.collections.user_transaction_details.retrieve', + 'eg_auth_token param.start param.end param.shortname every_param.usrid' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsGetPatronDetail','usrid','path','integer',NULL,TRUE), + ('collectionsGetPatronDetail','shortname','path','string',NULL,TRUE), + ('collectionsGetPatronDetail','start','query','string','date-time',TRUE), + ('collectionsGetPatronDetail','end','query','string','date-time',TRUE) +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items,validate) VALUES ('collectionsGetPatronDetail','array','object',FALSE) ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsPutPatronInCollections', + '/patron/:usrid/collections/:shortname', + 'post', + 'Put patron into collections.', + 'open-ils.collections', + 'open-ils.collections.put_into_collections', + 'eg_auth_token param.usrid param.shortname param.fee param.note' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,schema_format,required) VALUES + ('collectionsPutPatronInCollections','usrid','path','integer',NULL,TRUE), + ('collectionsPutPatronInCollections','shortname','path','string',NULL,TRUE), + ('collectionsPutPatronInCollections','fee','query','string','money',FALSE), + ('collectionsPutPatronInCollections','note','query','string',NULL,FALSE) +ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES ( + 'collectionsRemovePatronFromCollections', + '/patron/:usrid/collections/:shortname', + 'delete', + 'Remove patron from collections.', + 'open-ils.collections', + 'open-ils.collections.remove_from_collections', + 'eg_auth_token param.usrid param.shortname' +) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES + ('collectionsRemovePatronFromCollections','usrid','path','integer',TRUE), + ('collectionsRemovePatronFromCollections','shortname','path','string',TRUE) +ON CONFLICT DO NOTHING; + + +------- courses +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('activeCourses', '/courses', 'get', 'Retrieve all courses used for course material reservation', 'OpenILS::OpenAPI::Controller::course', 'get_active_courses', 'eg_auth_token every_param.org') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeCourses','org','query','integer') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeCourses','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('activeRoles', '/courses/public_role_users', 'get', 'Retrieve all public roles used for courses', 'OpenILS::OpenAPI::Controller::course', 'get_all_course_public_roles', 'eg_auth_token every_param.org') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type) VALUES ('activeRoles','org','query','integer') ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('activeRoles','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourse', '/course/:id', 'get', 'Retrieve one detailed course', 'OpenILS::OpenAPI::Controller::course', 'get_course_detail', 'eg_auth_token param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourse','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,fm_type) VALUES ('retrieveCourse','acmc') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourseMaterials', '/course/:id/materials', 'get', 'Retrieve detailed materials for one course', 'OpenILS::OpenAPI::Controller::course', 'get_course_materials', 'param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseMaterials','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseMaterials','array','object') ON CONFLICT DO NOTHING; + +INSERT INTO openapi.endpoint (operation_id, path, http_method, summary, method_source, method_name, method_params) VALUES + ('retrieveCourseUsers', '/course/:id/public_role_users', 'get', 'Retrieve detailed user list for one course', 'open-ils.courses', 'open-ils.courses.course_users.retrieve', 'param.id') +ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_param (endpoint,name,in_part,schema_type,required) VALUES ('retrieveCourseUsers','id','path','integer',TRUE) ON CONFLICT DO NOTHING; +INSERT INTO openapi.endpoint_response (endpoint,schema_type,array_items) VALUES ('retrieveCourseUsers','array','object') ON CONFLICT DO NOTHING; + +--------- put likely stock endpoints into sets -------- + +INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) + SELECT e.operation_id, s.name FROM openapi.endpoint e JOIN openapi.endpoint_set s ON (e.path LIKE '/'||RTRIM(s.name,'s')||'%') +ON CONFLICT DO NOTHING; + +-- Check that all endpoints are in sets -- should return 0 rows +SELECT * FROM openapi.endpoint e WHERE NOT EXISTS (SELECT 1 FROM openapi.endpoint_set_endpoint_map WHERE endpoint = e.operation_id); + +-- Global Fieldmapper property-filtering org and user settings +INSERT INTO config.settings_group (name,label) VALUES ('openapi','OpenAPI data access control'); + +INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi'); +INSERT INTO config.org_unit_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi'); + +UPDATE config.org_unit_setting_type + SET update_perm = (SELECT id FROM permission.perm_list WHERE code = 'ADMIN_OPENAPI' LIMIT 1) + WHERE name IN ('REST.api.blacklist_properties','REST.api.whitelist_properties'); + +INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.whitelist_properties','Globally whitelisted Fieldmapper properties','openapi'); +INSERT INTO config.usr_setting_type (name,label,grp) VALUES ('REST.api.blacklist_properties','Globally filtered Fieldmapper properties','openapi'); + +COMMIT; + +/* -- Some extra example permission setup, to allow (basically) readonly patron retrieve + +INSERT INTO permission.perm_list (code,description) VALUES ('REST.api.patrons.detail.read','Permission meant to facilitate read-only patron-related API access'); +INSERT INTO openapi.perm_set (name) values ('Patron Detail, Readonly'); +INSERT INTO openapi.perm_set_perm_map (perm_set,perm) SELECT s.id, p.id FROM openapi.perm_set s, permission.perm_list p WHERE p.code IN ('REST.api', 'REST.api.patrons.detail.read') AND s.name='Patron RO'; +INSERT INTO openapi.endpoint_perm_set_map (endpoint,perm_set) SELECT 'retrievePatronProfile', s.id FROM openapi.perm_set s WHERE s.name='Patron RO'; + +*/ diff --git a/Open-ILS/src/support-scripts/api_ctl b/Open-ILS/src/support-scripts/api_ctl new file mode 100755 index 0000000000..38db01ab14 --- /dev/null +++ b/Open-ILS/src/support-scripts/api_ctl @@ -0,0 +1,2043 @@ +#!/usr/bin/perl + +# --------------------------------------------------------------- +# Copyright (C) 2024 Equinox Open Library Initiative, Inc. +# Mike Rylander <mrylander@gmail.com> + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# --------------------------------------------------------------- + +use strict; +use warnings; + +use DBI; +use Getopt::Long; +use Data::Dumper; +use IO::Prompter; +use Digest::MD5 qw/md5_hex/; + +my $raise_db_error = 0; + +# Globals for the command line options: +my $opt_lockfile = '/tmp/api_ctl-LOCK'; +my $help = 0; +my $from_command = 0; + +# Database connection options with defaults: +my $db_user = $ENV{PGUSER} || 'evergreen'; +my $db_host = $ENV{PGHOST} || 'localhost'; +my $db_db = $ENV{PGDATABASE} || 'evergreen'; +my $db_pw = $ENV{PGPASSWORD} || 'evergreen'; +my $db_port = $ENV{PGPORT} || 5432; + +# State globals +my %state; +my @command_stack; + + +GetOptions( + 'lock-file=s' => \$opt_lockfile, + 'dbuser=s' => \$db_user, + 'dbhost=s' => \$db_host, + 'dbname=s' => \$db_db, + 'dbpw=s' => \$db_pw, + 'dbport=i' => \$db_port, + 'dbraise-error' => \$raise_db_error, + 'command' => sub { $from_command++; die '!FINISH' }, # this is similar to --, to leave the rest of @ARGV alone, but also know that it's meant to be a one-shot + 'help' => \$help +); + +help() if ($help); + +sub help { + print <<HELP; + +...man page here... + +HELP + exit; +} + +my $dsn = "dbi:Pg:dbname=$db_db;host=$db_host;port=$db_port;application_name='api_ctl';sslmode=allow"; + +my $dbh = DBI->connect( + $dsn, $db_user, $db_pw, + { AutoCommit => 1, + pg_expand_array => 0, + pg_bool_tf => 0, + RaiseError => $raise_db_error + } +) || die "Could not connect to the database\n"; + +push(@ARGV, 'quit') if $from_command; +exit next_cmd('OpenAPI:' => [qw/api integrator control details/]); + +#------------------ + +sub prompt_user_input_return_first { + my $next = prompt @_; + $next =~ s/^\s+//; + $next =~ s/\s+$//; + my @parts = split /\s+/, $next; + $next = shift @parts; + push @ARGV, @parts; + return $next +} + +sub list_or_argv { + my $prompt = shift; + my $options = shift; + return shift @ARGV if @ARGV; + print "\n* Command path: " . join('/', @command_stack) . "\n" if @command_stack and !$from_command; + return prompt_user_input_return_first($prompt => -stdio, -comp => $options, @_); +} + +sub number_menu_or_argv { + return shift @ARGV if @ARGV; + return menu_or_argv(@_, -number); +} + +sub menu_or_argv { + return shift @ARGV if @ARGV; + my $prompt = shift; + my $options = shift; + print "\n* Command path: " . join('/', @command_stack) . "\n" if @command_stack and !$from_command; + return prompt_user_input_return_first($prompt => -stdio, -menu => $options, @_); +} + +sub menu_spec_from_table { + my $table = shift; + my $label = shift; + my $value = shift; + my $no_value_label = shift; + my $no_value_value = shift; + + my $spec = {}; + $$spec{" $no_value_label"} = $no_value_value if $no_value_label; + + for my $row ($dbh->selectall_array("SELECT $label as \"$label\", $value FROM $table", { Slice => {} })) { + $$spec{$$row{$label}} = $$row{$value}; + } + + return $spec; +} + +sub permission_menu_spec { + return { map { $_->{code} . ': ' . $_->{description} => $_->{id} } $dbh->selectall_array('SELECT * FROM permission.perm_list', { Slice => {} }) }; +} + +sub depth_menu_spec { + return menu_spec_from_table('actor.org_unit_type','name','depth'); +} + +sub value_or_argv { + my $prompt = shift; + return shift @ARGV if @ARGV; + return prompt $prompt, -stdio, -verbatim, @_; +} + +sub prompted_value { + my $prompt = shift; + return prompt -prompt => $prompt, -stdio, -verbatim, @_; +} + +sub pw_value { + my $prompt = shift; + return prompted_value($prompt, -echo => '*'); +} + +sub yn { + my $prompt = shift; + return prompt $prompt, -stdio, -yes, @_; +} + +sub next_cmd { + my $prompt = shift; + my $options = shift; + push @$options, 'back' if @command_stack; + push @$options, 'show', 'quit'; + my $result = 0; + while (my $cmd = list_or_argv($prompt => $options)) { + if ($cmd eq 'top') { # special case to go back all the way to the top + if (@command_stack) { + unshift @ARGV, 'top'; + last; + } + next; + } + + next if ($cmd eq 'show' and show_state(1)); # special case to show the loaded state + last if ($cmd eq 'back'); # special case to go back one command level + quit() if ($cmd eq 'quit'); # special case to leave + + unless (grep {$_ eq $cmd} @$options) { + warn "Command [$cmd] not known\n" if ($cmd ne ''); + next; + } + + push @command_stack, $cmd; + my $subname = join '_', @command_stack; + $result = &{\&{$subname}}(); + pop @command_stack; + + } + return $result; +} + +sub quit { exit } + +sub show_state { + my $immediate_details = shift; + + print '-' x 50 ."\n"; + my $sep = 0; + + if ($state{control}{perm_set}) { + print "\n" if $sep; + print " == Permission Set Name: $state{control}{perm_set}{name}\n"; + if ($immediate_details || $state{details} || $state{control_details}|| $state{control_perm_sets_details}) { + print " -- ID: $state{control}{perm_set}{id}\n"; + print " -- Permissions: ".join(', ', map {$$_{code}} @{$state{control}{perm_set}{perms}})."\n"; + print " -- Endpoints: ".join(', ', @{$state{control}{perm_set}{endpoints}})."\n"; + print " -- Sets: ".join(', ', @{$state{control}{perm_set}{sets}})."\n"; + } + $sep++; + } + + if ($state{control}{rate_limit}) { + print "\n" if $sep; + print " == Rate Limit Definition Name: $state{control}{rate_limit}{name}\n"; + if ($immediate_details || $state{details} || $state{control_details}|| $state{control_rate_limit_details}) { + print " -- ID: $state{control}{rate_limit}{id}\n"; + print " -- Count: $state{control}{rate_limit}{limit_count}\n"; + print " -- Interval: $state{control}{rate_limit}{limit_interval}\n"; + print " -- Endpoints: ".join(', ', @{$state{control}{rate_limit}{endpoints}})."\n"; + print " -- Sets: ".join(', ', @{$state{control}{rate_limit}{sets}})."\n"; + } + $sep++; + } + + if ($state{integrator}) { + print "\n" if $sep; + print " == Integrator username: $state{integrator}{usrname}\n"; + if ($immediate_details || $state{details} || $state{integrator_details}) { + print " -- Lead Account: ".($state{integrator}{master_account} ? 'Yes' : 'No')."\n"; + print " -- Account Group ID: $state{integrator}{usrgroup}\n"; + print " -- Main Permission Group: $state{integrator}{profile}{name}\n"; + print " -- Secondary Perm Groups: ".join(', ', map {$$_{name}} @{$state{integrator}{secondary_groups}})."\n"; + print " -- Permissions: ".join(', ', map {$$_{code}} @{$state{integrator}{user_perms}})."\n"; + print " -- Rate Limited Endpoints: ".join(', ', map {"$$_{endpoint} => $$_{name}"} @{$state{integrator}{endpoint_rate_limits}})."\n"; + print " -- Rate Limited Sets: ".join(', ', map {"$$_{endpoint_set} => $$_{name}"} @{$state{integrator}{endpoint_set_rate_limits}})."\n"; + print " -- Property Filters: ".join(', ', map {"$$_{name} => $$_{value}"} @{$state{integrator}{filter_settings}})."\n"; + print " -- Name: $state{integrator}{first_given_name} $state{integrator}{family_name}\n"; + print " -- Barcode: $state{integrator}{card}{barcode}\n" if $state{integrator}{card}; + print " -- Email: $state{integrator}{email}\n" if $state{integrator}{email}; + print " -- Day Phone: $state{integrator}{day_phone}\n" if $state{integrator}{day_phone}; + if ( my $s = $state{integrator}{login_attempt_summary}) { + print " :: Login Attempt Summary\n"; + print " . First Attempt: $$s{first_attempt}\n"; + print " . Last Attempt: $$s{last_attempt}\n"; + print " . Success Count: $$s{success}\n"; + print " . Fail Count: $$s{failure}\n"; + } + if (@{$state{integrator}{endpoint_access_summary}}) { + print " :: Endpoint Access Summary\n"; + print " . $$_{endpoint}: First: $$_{first_attempt}, Last: $$_{last_attempt}, Success: $$_{success}, Failure: $$_{failure}\n" + for (@{$state{integrator}{endpoint_access_summary}}); + } + } + $sep++; + } + + if ($state{api}{set}) { + print "\n" if $sep; + print " == API Endpoint Set: $state{api}{set}{name}\n"; + if ($immediate_details || $state{details} || $state{api_details} || $state{api_sets_details}) { + print " -- Description: $state{api}{set}{description}\n"; + print " -- Active: ".($state{api}{set}{active} ? 'Yes' : 'No')."\n"; + print " -- Rate Limit: $state{api}{set}{rate_limit}{name}\n" if $state{api}{set}{rate_limit}; + print " -- IP Ranges: ".join(', ', map {$$_{ip_range}} @{$state{api}{set}{ip_ranges}})."\n"; + print " -- Endpoints: ".join(', ', @{$state{api}{set}{endpoints}})."\n"; + print " -- Permissions: ".join(', ', map {$$_{code}} @{$state{api}{set}{perms}})."\n"; + print " -- Perm Sets: ".join(', ', map {$$_{name}.' ('.join(' ', map{$$_{code}} @{$$_{perms}}).')'} @{$state{api}{set}{perm_sets}})."\n"; + } + $sep++; + } + + if ($state{api}{endpoint}) { + print "\n" if $sep; + print " == API Endpoint Operation ID: $state{api}{endpoint}{operation_id}\n"; + if ($immediate_details || $state{details} || $state{api_details} || $state{api_endpointss_details}) { + print " -- Summary: $state{api}{endpoint}{summary}\n"; + print " -- Active: ".($state{api}{endpoint}{active} ? 'Yes' : 'No')."\n"; + print " -- Path: $state{api}{endpoint}{path}\n"; + print " -- HTTP Method: $state{api}{endpoint}{http_method}\n"; + print " -- Security: $state{api}{endpoint}{security}\n"; + print " -- Source: $state{api}{endpoint}{method_source}\n"; + print " -- Handler: $state{api}{endpoint}{method_name}\n"; + print " -- Param Map: $state{api}{endpoint}{method_params}\n"; + print " -- Rate Limit: $state{api}{endpoint}{rate_limit}{name}\n" if $state{api}{endpoint}{rate_limit}; + print " -- IP Ranges: ".join(', ', map {$$_{ip_range}} @{$state{api}{endpoint}{ip_ranges}})."\n"; + print " -- Sets: ".join(', ', @{$state{api}{endpoint}{sets}})."\n"; + print " -- Permissions: ".join(', ', map {$$_{code}} @{$state{api}{endpoint}{perms}})."\n"; + print " -- Perm Sets: ".join(', ', map {$$_{name}.' ('.join(' ', map{$$_{code}} @{$$_{perms}}).')'} @{$state{api}{endpoint}{perm_sets}})."\n"; + if (@{$state{api}{endpoint}{params}}) { + print " :: Parameters\n"; + for my $p (@{$state{api}{endpoint}{params}}) { + print " >> Name: $$p{name}\n"; + print " . In: $$p{in_part}\n"; + print " . Required: ".($$p{required} ? 'Yes' : 'No')."\n"; + print " . Fieldmapper Type: $$p{fm_type}\n" if $$p{fm_type}; + print " . JSON Schema Type: $$p{schema_type}\n" if $$p{schema_type}; + print " . JSON Schema Format: $$p{schema_format}\n" if $$p{schema_format}; + print " . Array Items Type: $$p{array_items}\n" if $$p{array_items}; + print " . Default Value: $$p{default_value}\n" if $$p{default_value}; + } + } + if (@{$state{api}{endpoint}{responses}}) { + print " :: Responses\n"; + for my $p (@{$state{api}{endpoint}{responses}}) { + print " >> HTTP Status Code: $$p{status}\n"; + print " . Content Type: $$p{content_type}\n"; + print " . Validate Output: ".($$p{validate} ? 'Yes' : 'No')."\n"; + print " . Description: $$p{description}\n"; + print " . Fieldmapper Type: $$p{fm_type}\n" if $$p{fm_type}; + print " . JSON Schema Type: $$p{schema_type}\n" if $$p{schema_type}; + print " . JSON Schema Format: $$p{schema_format}\n" if $$p{schema_format}; + print " . Array Items Type: $$p{array_items}\n" if $$p{array_items}; + } + } + } + $sep++; + } + + print '-' x 50 ."\n"; + return 1; +} + +sub details { + $state{details} = $state{details} ? 0 : 1; + print ' ! Global Detail Dispaly: '. ($state{details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub control { + next_cmd('Control actions:' => [qw/rate_limit perm_sets details/]) +} + +sub control_details { + $state{control_details} = $state{control_details} ? 0 : 1; + print ' ! Control Detail Dispaly: '. ($state{control_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub control_perm_sets { + next_cmd('Control Permission Sets actions:' => [qw/list load unload add remove edit details permissions/]) +} + +sub control_perm_sets_permissions { + control_perm_sets_load() unless $state{control}{perm_set}{id}; + next_cmd('Control Permission Sets Permission actions:' => [qw/list add remove/]) if $state{control}{perm_set}{id}; +} + +sub control_perm_sets_permissions_list { + print "Permission in Set $state{control}{perm_set}{name}\n" + . "=================================\n" if !$from_command; + print join( + "\n", + map { + "$$_{code}: $$_{description}" + } @{$state{control}{perm_set}{perms}} + ) . "\n"; +} + +sub control_perm_sets_permissions_add { + my $new_perm = list_or_argv('New Permission:' => $dbh->selectcol_arrayref('SELECT code FROM permission.perm_list')); + if ($new_perm ne '' and yn("Add permission [$new_perm] to Permission Set [$state{control}{perm_set}{name}]?")) { + $new_perm = $dbh->selectcol_arrayref('SELECT id FROM permission.perm_list WHERE code = ?', undef, $new_perm)->[0]; + $dbh->do('INSERT INTO openapi.perm_set_perm_map (perm_set,perm) VALUES (?,?)', undef, $state{control}{perm_set}{id}, $new_perm); + reload_control_perm_sets(); + } +} + + +sub control_perm_sets_permissions_remove { + if (@{$state{control}{perm_set}{perms}}) { + my $perm = list_or_argv('Permission:' => [map {$$_{code}} @{$state{control}{perm_set}{perms}}]); + if ($perm ne '' and yn("Remove permission [$perm] from Permission Set [$state{control}{perm_set}{name}]?")) { + $perm = $dbh->selectcol_arrayref('SELECT id FROM permission.perm_list WHERE code = ?', undef, $perm)->[0]; + $dbh->do('DELETE FROM openapi.perm_set_perm_map WHERE perm_set = ? AND perm = ?', undef, $state{control}{perm_set}{id}, $perm) if ($perm); + reload_control_perm_sets(); + } + } +} + +sub control_perm_sets_details { + $state{control_perm_sets_details} = $state{control_perm_sets_details} ? 0 : 1; + print ' ! Control Detail Dispaly: '. ($state{control_perm_sets_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub control_perm_sets_list { + print "Permission Sets:\n" + . "================\n" if !$from_command; + print join( + "\n", + map { + "$$_{id}: $$_{name}" + } $dbh->selectall_array('SELECT id,name FROM openapi.perm_set ORDER BY name',{Slice => {}}) + ) . "\n"; +} + +sub control_perm_sets_unload { + delete $state{control}{perm_set}; +} + +sub reload_control_perm_sets { + if ($state{control}{perm_set}{id}) { + unshift @ARGV, $state{control}{perm_set}{id}; + control_perm_sets_load(); + } +} + +sub control_perm_sets_load { + my $id = shift || number_menu_or_argv( + 'Permission Set:' => menu_spec_from_table('openapi.perm_set', 'name', 'id', '[Cancel]', 0) + ); + + control_perm_sets_unload(); + return {} if ($id eq '0'); + + $state{control}{perm_set} = $dbh->selectrow_hashref('SELECT * FROM openapi.perm_set WHERE id = ?',undef,$id); + + $state{control}{perm_set}{perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM openapi.perm_set_perm_map m JOIN permission.perm_list p ON (m.perm = p.id) WHERE m.perm_set = ? ORDER BY p.code', + {Slice => {}}, $state{control}{perm_set}{id} + ); + + $state{control}{perm_set}{endpoints} = $dbh->selectcol_arrayref( + 'SELECT endpoint FROM openapi.endpoint_perm_set_map WHERE perm_set = ?', + undef, $state{control}{perm_set}{id} + ); + + $state{control}{perm_set}{sets} = $dbh->selectcol_arrayref( + 'SELECT endpoint_set FROM openapi.endpoint_set_perm_set_map WHERE perm_set = ?', + undef, $state{control}{perm_set}{id} + ); + + + return $state{control}{perm_set}; +} + +sub control_perm_sets_edit { + my $rl = control_perm_sets_load($state{control}{perm_set}{id}); + if ($rl) { + $$rl{name} = prompt "Name [$$rl{name}]:", -def => $$rl{name}; + $dbh->do("UPDATE openapi.perm_set SET name = ? WHERE id = ?", undef, $$rl{name}, $$rl{id}); + reload_control_perm_sets(); + } +} + +sub control_perm_sets_add { + control_perm_sets_unload(); + my $name = prompt "Name:"; + $dbh->do("INSERT INTO openapi.perm_sets (name) VALUES (?)", undef, $name); + control_perm_sets_list(); +} + +sub control_perm_sets_remove { + if ($state{control}{perm_set}{id} and yn("Continue to remove Permission Set $state{control}{perm_set}{name}?")) { + $dbh->do( "DELETE FROM openapi.perm_set WHERE id = ?", undef, $state{control}{perm_set}{id} ); + control_perm_sets_unload(); + control_perm_sets_list(); + } +} + + +sub control_rate_limit { + next_cmd('Control Rate Limit actions:' => [qw/list load unload add remove edit details/]) +} + +sub control_rate_limit_details { + $state{control_rate_limit_details} = $state{control_rate_limit_details} ? 0 : 1; + print ' ! Control Detail Dispaly: '. ($state{control_rate_limit_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub control_rate_limit_list { + print "Rate Limit Definitions:\n" + . "=======================\n" if !$from_command; + print join( + "\n", + map { + "$$_{id}: $$_{name}" + } $dbh->selectall_array('SELECT id,name FROM openapi.rate_limit_definition ORDER BY name',{Slice => {}}) + ) . "\n"; +} + + +sub control_rate_limit_unload { + delete $state{control}{rate_limit}; +} + +sub reload_control_rate_limit { + if ($state{control}{rate_limit}{id}) { + unshift @ARGV, $state{control}{rate_limit}{id}; + control_rate_limit_load(); + } +} + +sub control_rate_limit_load { + my $id = shift || number_menu_or_argv( + 'Rate Limit Definition:' => menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[Cancel]', 0) + ); + + control_rate_limit_unload(); + return {} if ($id eq '0'); + + $state{control}{rate_limit} = $dbh->selectrow_hashref('SELECT * FROM openapi.rate_limit_definition WHERE id = ?',undef,$id); + + $state{control}{rate_limit}{endpoint_ip_ranges} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_ip_rate_limit_map WHERE rate_limit = ?', + { Slice => {} }, $state{control}{rate_limit}{id} + ); + + $state{control}{rate_limit}{endpoint_set_ip_ranges} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_set_ip_rate_limit_map WHERE rate_limit = ?', + { Slice => {} }, $state{control}{rate_limit}{id} + ); + + $state{control}{rate_limit}{endpoint_users} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_user_rate_limit_map WHERE rate_limit = ?', + { Slice => {} }, $state{control}{rate_limit}{id} + ); + + $state{control}{rate_limit}{endpoint_set_users} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_set_user_rate_limit_map WHERE rate_limit = ?', + { Slice => {} }, $state{control}{rate_limit}{id} + ); + + $state{control}{rate_limit}{endpoints} = $dbh->selectcol_arrayref( + 'SELECT operation_id FROM openapi.endpoint WHERE rate_limit = ?', + undef, $state{control}{rate_limit}{id} + ); + + $state{control}{rate_limit}{sets} = $dbh->selectcol_arrayref( + 'SELECT name FROM openapi.endpoint_set WHERE rate_limit = ?', + undef, $state{control}{rate_limit}{id} + ); + + + return $state{control}{rate_limit}; +} + +sub control_rate_limit_edit { + my $rl = control_rate_limit_load($state{control}{rate_limit}{id}); + if ($rl) { + $$rl{name} = prompt "Name [$$rl{name}]:", -def => $$rl{name}; + $$rl{limit_count} = prompt "Count [$$rl{limit_count}]:", -def => $$rl{limit_count}; + $$rl{limit_interval} = prompt "Interval [$$rl{limit_interval}]:", -def => $$rl{limit_interval}; + $dbh->do("UPDATE openapi.rate_limit_definition SET name = ?, limit_count = ?, limit_interval = ? WHERE id = ?", undef, $$rl{name}, $$rl{limit_count}, $$rl{limit_interval}, $$rl{id}); + reload_control_rate_limit(); + } +} + +sub control_rate_limit_add { + control_rate_limit_unload(); + my $name = prompt "Name:"; + my $limit_count = prompt "Count:"; + my $limit_interval = prompt "Interval:"; + $dbh->do("INSERT INTO openapi.rate_limit_definition (name, limit_count, limit_interval) VALUES (?,?,?)", undef, $name, $limit_count, $limit_interval); + control_rate_limit_list(); +} + +sub control_rate_limit_remove { + if ($state{control}{rate_limit}{id} and yn("Continue to remove rate limit definition $state{control}{rate_limit}{name}?")) { + $dbh->do( "DELETE FROM openapi.rate_limit_definition WHERE id = ?", undef, $state{control}{rate_limit}{id} ); + control_rate_limit_unload(); + control_rate_limit_list(); + } +} + +sub api { + next_cmd('API actions:' => [qw/endpoints sets details/]) +} + +sub api_details { + $state{api_details} = $state{api_details} ? 0 : 1; + print ' ! API Detail Dispaly: '. ($state{api_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub api_endpoints { + next_cmd('API Endpoint actions:' => [qw/list load unload add remove edit activate deactivate parameters responses sets rate_limits perm_sets/]) +} + +sub _collect_endpoint_data { + my $ep = shift || { + operation_id => '', + summary => '', + path => '', + http_method => 'get', + security => 'bearerAuth', + method_source=> '', + method_name => '', + method_params=> 'eg_auth_token', + active => 0, + rate_limit => { name => 'None', id => 0 } + }; + + + + $$ep{operation_id} = prompt "Operation ID".($$ep{operation_id} ? " [$$ep{operation_id}]" :'').":", -def => $$ep{operation_id} || ''; + $$ep{summary} = prompt "Summary".($$ep{summary} ? " [$$ep{summary}]" :'').":", -def => $$ep{summary} || ''; + $$ep{path} = prompt "Path".($$ep{path} ? " [$$ep{path}]" :'').":", -def => $$ep{path} || ''; + $$ep{http_method} = prompt "HTTP Method [$$ep{http_method}]:", -def => $$ep{http_method}, -comp => [qw/get post put delete patch/]; + $$ep{security} = prompt "Security [$$ep{security}]:", -def => $$ep{security}, -comp => [map {$_.'Auth'} qw/bearer basic cookie param/]; + $$ep{method_source}= prompt "Method Source".($$ep{method_source} ? " [$$ep{method_source}]" :'').":", -def => $$ep{method_source} || ''; + $$ep{method_name} = prompt "Method Name".($$ep{method_name} ? " [$$ep{method_name}]" :'').":", -def => $$ep{method_name} || ''; + $$ep{method_params}= prompt "Method Params".($$ep{method_params} ? " [$$ep{method_params}]" :'').":", -def => $$ep{method_params} || ''; + $$ep{active} = prompt "Active [".($$ep{active} ? 'Y' : 'N')."]:", -yn => -def => ($$ep{active} ? 'y':'n'); + $$ep{rate_limit} = number_menu_or_argv( + "Rate Limit".($$ep{rate_limit}? " [$$ep{rate_limit}{name}]" : '').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[None]', 0), + -def => (ref $$ep{rate_limit} ? $$ep{rate_limit}{id} : 0) + ); + + $$ep{active} = $$ep{active} eq 'y' ? 1 : 0; + $$ep{rate_limit} = $$ep{rate_limit} eq '0' ? undef : $$ep{rate_limit}; + + return $ep; +} + +sub api_endpoints_edit { + my $ep = $state{api}{endpoint} || api_endpoints_load(); + if ($ep) { + my $oldname = $$ep{operation_id}; + _collect_endpoint_data($ep); + $dbh->do( + "UPDATE openapi.endpoint". + " SET operation_id=?, summary=?, active=?, rate_limit=?, path=?, http_method=?,". + " security=?, method_source=?, method_name=?, method_params=?". + " WHERE operation_id = ?", undef, + $$ep{operation_id}, $$ep{summary}, $$ep{active}, $$ep{rate_limit}, $$ep{path}, $$ep{http_method}, + $$ep{security}, $$ep{method_source}, $$ep{method_name}, $$ep{method_params}, $oldname + ); + reload_api_endpoints(); + } +} + +sub api_endpoints_remove { + if ($state{api}{endpoint}{operation_id} and yn("Continue to remove endpoint $state{api}{endpoint}{operation_id}?")) { + $dbh->do( "DELETE FROM openapi.endpoint WHERE operation_id = ?", undef, $state{api}{endpoint}{operation_id} ); + api_endpoints_unload(); + api_endpoints_list(); + } +} + +sub api_endpoints_add { + api_endpoints_unload(); + my $ep = _collect_endpoint_data(); + $dbh->do( + "INSERT INTO openapi.endpoint (operation_id, summary, path, http_method, security, method_source, method_name, method_params, active, rate_limit) ". + " VALUES (?,?,?,?,?,?,?,?,?,?)", undef, + $$ep{operation_id}, $$ep{summary}, $$ep{path}, $$ep{http_method}, $$ep{security}, $$ep{method_source}, $$ep{method_name}, $$ep{method_params}, $$ep{active}, $$ep{rate_limit} + ); + unshift @ARGV, $$ep{operation_id}; + api_endpoints_load(); +} + +sub api_endpoints_details { + $state{api_endpoints_details} = $state{api_endpoints_details} ? 0 : 1; + print ' ! API Endpoint Detail Dispaly: '. ($state{api_endpoints_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub api_endpoints_list { + print "API Endpoints:\n" + . "==============\n" if !$from_command; + print join( + "\n", + map { + "$$_{operation_id}: [$$_{http_method} $$_{path}] $$_{summary} " . ($$_{active} ? '(Active)' : '(Inactive)') + } $dbh->selectall_array('SELECT * FROM openapi.endpoint ORDER BY operation_id',{Slice => {}}) + ) . "\n"; +} + +sub api_endpoints_activate { + my $enabled = shift || 'TRUE'; + my $oid = $state{api}{endpoint}{operation_id} || value_or_argv('Endpoint Operation ID:'); + if ($oid ne '') { + $dbh->do("UPDATE openapi.endpoint SET active = $enabled WHERE operation_id = ?", undef, $oid); + unshift @ARGV, $oid; + api_endpoints_load(); + } +} +sub api_endpoints_deactivate { api_endpoints_activate('FALSE') } + +sub api_endpoints_sets { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + next_cmd('API Endpoint Assigned Sets actions:' => [qw/list add remove/]) +} + +sub api_endpoints_sets_list { + print "API Sets for Endpoint\n" + . "=====================\n" if !$from_command; + print join("\n", @{$state{api}{endpoint}{sets}})."\n"; +} + +sub api_endpoints_sets_remove { + return unless @{$state{api}{endpoint}{sets}}; + if (my $s = value_or_argv('API Set:', -comp => $state{api}{endpoint}{sets})) { + if (yn("Remove endpoint $state{api}{endpoint}{operation_id} from set $s?")) { + $dbh->do( "DELETE FROM openapi.endpoint_set_endpoint_map WHERE endpoint = ? AND endpoint_set = ?", undef, $state{api}{endpoint}{operation_id}, $s ); + reload_api_endpoints(); + } + } +} + +sub api_endpoints_sets_add { + my $options = $dbh->selectcol_arrayref( + 'SELECT s.name FROM openapi.endpoint_set s WHERE name NOT IN (SELECT endpoint_set FROM openapi.endpoint_set_endpoint_map WHERE endpoint = ?)', + undef, $state{api}{endpoint}{operation_id} + ); + return unless @$options; + if (my $s = value_or_argv('API Set:', -comp => $options)) { + if (yn("Add endpoint $state{api}{endpoint}{operation_id} to set $s?")) { + $dbh->do( + "INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) VALUES (?,?)", + undef, $state{api}{endpoint}{operation_id}, $s + ); + reload_api_endpoints(); + } + } +} + +sub api_endpoints_perm_sets { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + next_cmd('API Endpoint Permission Set actions:' => [qw/list add remove/]) +} + +sub api_endpoints_perm_sets_list { + print "Permission Sets for Endpoint\n" + . "============================\n" if !$from_command; + print join("\n", map { $$_{name} .': '. join(', ', map {$$_{code}} @{$$_{perms}}) } @{$state{api}{endpoint}{perm_sets}})."\n"; +} + +sub api_endpoints_perm_sets_remove { + return unless @{$state{api}{endpoint}{perm_sets}}; + if (my $s = number_menu_or_argv('Permission Set:', { map {$$_{name} => $$_{id}} @{$state{api}{endpoint}{perm_sets}} })) { + $dbh->do( "DELETE FROM openapi.endpoint_perm_set_map WHERE endpoint = ? AND perm_set = ?", undef, $state{api}{endpoint}{operation_id}, $s ); + reload_api_endpoints(); + } +} + +sub api_endpoints_perm_sets_add { + my $options = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.perm_set WHERE id NOT IN (SELECT perm_set FROM openapi.endpoint_perm_set_map WHERE endpoint = ?)', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + return unless @$options; + if (my $s = number_menu_or_argv('Permission Set:', { map {$$_{name} => $$_{id}} @$options })) { + $dbh->do( + "INSERT INTO openapi.endpoint_perm_set_map (endpoint, perm_set) VALUES (?,?)", + undef, $state{api}{endpoint}{operation_id}, $s + ); + reload_api_endpoints(); + } +} + + +sub api_endpoints_parameters { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + next_cmd('API Endpoint Parameter actions:' => [qw/list add edit remove/]) +} + +sub api_endpoints_parameters_list { + print "Parameters for Endpoint\n" + . "=======================\n" if !$from_command; + print join("\n", + map { "$$_{name}: Type [". + ($$_{fm_type}||$$_{schema_type}). + "] supplied in $$_{in_part}, ". + ($$_{required}?'':'not ').'required'. + (defined($$_{default_value}) ? "; Default [$$_{default_value}]" : '.') + } @{$state{api}{endpoint}{params}} + )."\n"; +} + +sub api_endpoints_parameters_remove { + return unless @{$state{api}{endpoint}{params}}; + if (my $p = number_menu_or_argv('Endpoint Parameter', {' Cancel' => 0, map {$$_{name}, $$_{id}} @{$state{api}{endpoint}{params}}})) { + if ($p ne '0' and yn("Remove parameter from endpoint $state{api}{endpoint}{operation_id}?")) { + $dbh->do( "DELETE FROM openapi.endpoint_param WHERE id = ?", undef, $p ); + reload_api_endpoints(); + } + } +} + +sub _collect_endpoint_parameter_data { + my $param = shift || { + name => '', + in_part => 'query', + required => 0, + fm_type => undef, + schema_type => 'string', + schema_format => undef, + array_items => undef, + default_value => undef + }; + + $$param{name} = prompt 'Name'.($$param{name}?" [$$param{name}]":'').':', -def => $$param{name} || ''; + $$param{in_part} = list_or_argv("In part [$$param{in_part}]:" => [qw/path query header cookie/], -def => $$param{in_part}); + $$param{required} = prompt "Required [".($$param{required}?'Y':'N')."]:", -def => $$param{requried} ? 'y':'n', -yn; + $$param{fm_type} = prompt 'Fieldmapper type (empty for none)'.($$param{fm_type}?" [$$param{fm_type}]":'').':', -def => $$param{fm_type} || ''; + $$param{schema_type} = ($$param{fm_type} eq '') ? + menu_or_argv( + "JSON Schema type".($$param{schema_type}?" [$$param{schema_type}]":'').':', + menu_spec_from_table('openapi.json_schema_datatype', 'label','name','[None]',''), -def => $$param{schema_type} || '' + ) : ''; + $$param{schema_format} = menu_or_argv( + "JSON Schema format".($$param{schema_format}?" [$$param{schema_format}]":'').":", + menu_spec_from_table('openapi.json_schema_format', 'label','name','[None]',''), -def => $$param{schema_format} || '' + ); + $$param{array_items} = ($$param{schema_type} eq 'array') ? + menu_or_argv( + "Array item type".($$param{array_items}?" [$$param{array_items}]":'').":", + menu_spec_from_table('openapi.json_schema_datatype', 'label','name','[None]',''), -def => $$param{array_items} || '' + ) : ''; + $$param{default_value} = prompt 'Default value (empty for none)'.($$param{default_value}?" [$$param{default_value}]":'').':', -def => $$param{default_value} || ''; + + for my $nullable_part ( qw/fm_type schema_type schema_format array_items default_value/ ) { + $$param{$nullable_part} = undef if ($$param{$nullable_part} eq ''); + } + + return $param; +} + +sub api_endpoints_parameters_edit { + return unless @{$state{api}{endpoint}{params}}; + my $p = number_menu_or_argv('Endpoint Parameter', {' Cancel' => 0, map {$$_{name}, $$_{id}} @{$state{api}{endpoint}{params}}}); + if ($p ne '0') { + ($p) = grep { $$_{id} eq $p } @{$state{api}{endpoint}{params}}; + $p = _collect_endpoint_parameter_data($p); + $dbh->do('UPDATE openapi.endpoint_param'. + ' SET name = ?,'. + ' in_part = ?,'. + ' required = ?,'. + ' fm_type = ?,'. + ' schema_type = ?,'. + ' schema_format = ?,'. + ' array_items = ?,'. + ' default_value = ?'. + ' WHERE id = ?', undef, + $$p{name}, $$p{in_part}, $$p{required}, $$p{fm_type}, $$p{schema_type}, + $$p{schema_format}, $$p{array_items}, $$p{default_value}, $$p{id} + ); + reload_api_endpoints(); + } +} + +sub api_endpoints_parameters_add { + return unless $state{api}{endpoint}{operation_id}; + my $p = _collect_endpoint_parameter_data(); + $dbh->do('INSERT INTO openapi.endpoint_param (endpoint,name,in_part,required,fm_type,schema_type,schema_format,array_items,default_value)'. + ' VALUES (?,?,?,?,?,?,?,?,?)', undef, + $state{api}{endpoint}{operation_id}, $$p{name}, $$p{in_part}, $$p{required}, + $$p{fm_type}, $$p{schema_type}, $$p{schema_format}, $$p{array_items}, $$p{default_value} + ); + reload_api_endpoints(); +} + +sub api_endpoints_responses { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + next_cmd('API Endpoint Responses actions:' => [qw/list add edit remove/]) +} + +sub api_endpoints_responses_list { + print "Responses for Endpoint\n" + . "=======================\n" if !$from_command; + print join("\n", + map { "$$_{status}: $$_{description}; HTTP Content Type [$$_{content_type}], ". + "Data type [".($$_{fm_type}||$$_{schema_type}). "], ". + ($$_{validate}?'':'not ').'validated against OpenAPI spec' + } @{$state{api}{endpoint}{responses}} + )."\n"; +} + +sub api_endpoints_responses_remove { + return unless @{$state{api}{endpoint}{responses}}; + my $p = number_menu_or_argv( + 'Endpoint Response', + {' Cancel' => 0, + map { ("$$_{status}: $$_{description}", $$_{id}) } @{$state{api}{endpoint}{responses}} + } + ); + + if ($p ne '0' and yn("Remove response spec from endpoint $state{api}{endpoint}{operation_id}?")) { + $dbh->do( "DELETE FROM openapi.endpoint_response WHERE id = ?", undef, $p ); + reload_api_endpoints(); + } +} + +sub _collect_endpoint_response_data { + my $param = shift || { + status => '200', + content_type => 'application/json', + description => 'Success', + validate => 1, + fm_type => undef, + schema_type => undef, + schema_format => undef, + array_items => undef, + }; + + $$param{status} = prompt 'Status'.($$param{status}?" [$$param{status}]":'').':', -def => $$param{status} || ''; + $$param{content_type} = prompt + "HTTP Content Type [$$param{content_type}]:", + -comp => ['application/json', 'application/xml', 'text/plain', 'application/octet-stream'], + -def => $$param{content_type}; + $$param{description} = prompt 'Default value (empty for none)'.($$param{description}?" [$$param{description}]":'').':', -def => $$param{description} || ''; + $$param{validate} = prompt "Validate against OpenAPI spec [".($$param{validate}?'Y':'N')."]:", -def => $$param{validate} ? 'y':'n', -yn; + $$param{fm_type} = prompt 'Fieldmapper type (empty for none)'.($$param{fm_type}?" [$$param{fm_type}]":'').':', -def => $$param{fm_type} || ''; + $$param{schema_type} = ($$param{fm_type} eq '') ? + menu_or_argv( + "JSON Schema type".($$param{schema_type}?" [$$param{schema_type}]":'').':', + menu_spec_from_table('openapi.json_schema_datatype', 'label','name','[None]',''), -def => $$param{schema_type} || '' + ) : ''; + $$param{schema_format} = menu_or_argv( + "JSON Schema format".($$param{schema_format}?" [$$param{schema_format}]":'').":", + menu_spec_from_table('openapi.json_schema_format', 'label','name','[None]',''), -def => $$param{schema_format} || '' + ); + $$param{array_items} = ($$param{schema_type} eq 'array') ? + menu_or_argv( + "Array item type".($$param{array_items}?" [$$param{array_items}]":'').":", + menu_spec_from_table('openapi.json_schema_datatype', 'label','name','[None]',''), -def => $$param{array_items} || '' + ) : ''; + + for my $nullable_part ( qw/fm_type schema_type schema_format array_items/ ) { + $$param{$nullable_part} = undef if ($$param{$nullable_part} eq ''); + } + + return $param; +} + + +sub api_endpoints_responses_edit { + return unless @{$state{api}{endpoint}{responses}}; + my $p = number_menu_or_argv( + 'Endpoint Response', + {' Cancel' => 0, + map { ("$$_{status}: $$_{description}", $$_{id}) } @{$state{api}{endpoint}{responses}} + } + ); + if ($p ne '0') { + ($p) = grep { $$_{id} eq $p } @{$state{api}{endpoint}{responses}}; + $p = _collect_endpoint_response_data($p); + $dbh->do('UPDATE openapi.endpoint_response'. + ' SET status = ?,'. + ' content_type = ?,'. + ' description = ?,'. + ' validate = ?,'. + ' fm_type = ?,'. + ' schema_type = ?,'. + ' schema_format = ?,'. + ' array_items = ?'. + ' WHERE id = ?', undef, + $$p{status}, $$p{content_type}, $$p{description}, $$p{validate}, + $$p{fm_type}, $$p{schema_type}, $$p{schema_format}, $$p{array_items}, $$p{id} + ); + reload_api_endpoints(); + } +} + +sub api_endpoints_responses_add { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + my $p = _collect_endpoint_response_data(); + $dbh->do('INSERT INTO openapi.endpoint_response (endpoint,status,content_type,description,validate,fm_type,schema_type,schema_format,array_items)'. + ' VALUES (?,?,?,?,?,?,?,?,?)', undef, + $state{api}{endpoint}{operation_id}, $$p{status}, $$p{content_type}, $$p{description}, + $$p{validate}, $$p{fm_type}, $$p{schema_type}, $$p{schema_format}, $$p{array_items} + ); + reload_api_endpoints(); +} + +sub api_endpoints_rate_limits { + return unless $state{api}{endpoint}{operation_id} || api_endpoints_load(); + next_cmd('API Endpoint rate limit types:' => [qw/ip_address integrator/]) +} + +sub api_endpoints_rate_limits_ip_address { + next_cmd('API Endpoint IP Address rate limit commands:' => [qw/list add edit remove/]) +} + +sub api_endpoints_rate_limits_ip_address_list { + return api_endpoints_rate_limits_list('ip_ranges'); +} + +sub api_endpoints_rate_limits_integrator { + next_cmd('API Endpoint IP Address rate limit commands:' => [qw/list add edit remove/]) +} + +sub api_endpoints_rate_limits_integrator_list { + return api_endpoints_rate_limits_list('user_rate_limits'); +} + +sub get_endpoint_rate_limit_label_list { + my $type = shift; + my @pile; + for my $rl ( @{$state{api}{endpoint}{$type}} ) { + my $def = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.rate_limit_definition WHERE id = ?', + undef, $$rl{rate_limit} + ); + + my $limited = $type eq 'user_rate_limits' ? $dbh->selectrow_hashref( + 'SELECT usrname FROM actor.usr WHERE id = ?', + undef, $$rl{accessor} + )->{usrname} : $$rl{ip_range}; + + push @pile, {"$limited limited to $$def{limit_count} per $$def{limit_interval}" => $$rl{id}}; + } + return @pile; +} + +sub api_endpoints_rate_limits_list { + my $type = shift; + my $type_label = $type eq 'user_rate_limits' ? 'User-specific' : 'IP Range'; + + print "$type_label Endpoint Rate Limits for $state{api}{endpoint}{operation_id}\n" + . "===================================================\n" if !$from_command; + + print join("\n", map { + my $label = (keys(%$_))[0]; + $label = "$state{api}{endpoint}{operation_id}: $label" if $from_command; + $label; + } get_endpoint_rate_limit_label_list($type))."\n"; +} + +sub _collect_endpoint_rate_limit_ip_data { + my $ep = $state{api}{endpoint} || api_endpoints_load(); + return unless $ep; + + my $rl = shift || { + endpoint => $$ep{operation_id}, + ip_range => '', + rate_limit => undef + }; + + $$rl{endpoint} = prompt "Endpoint".($$rl{endpoint} ? " [$$rl{endpoint}]" :'').":", -def => $$rl{endpoint} || ''; + $$rl{ip_range} = prompt "CIDR IP Range".($$rl{ip_range} ? " [$$rl{ip_range}]" :'').":", -def => $$rl{ip_range} || ''; + $$rl{rate_limit} = number_menu_or_argv( + "Rate Limit".($$rl{rate_limit}? " [$$rl{rate_limit}{name}]" : '').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[Cancel]', 0), + -def => (ref $$rl{rate_limit} ? $$rl{rate_limit}{id} : 0) + ); + + $$rl{rate_limit} = $$rl{rate_limit} eq '0' ? undef : $$rl{rate_limit}; + + return $rl; +} + +sub api_endpoints_rate_limits_ip_address_add { + my $rl = _collect_endpoint_rate_limit_ip_data(); + $dbh->do( + "INSERT INTO openapi.endpoint_ip_rate_limit_map (endpoint, ip_range, rate_limit) VALUES (?,?,?)", undef, + $$rl{endpoint}, $$rl{ip_range}, $$rl{rate_limit} + ); + reload_api_endpoints(); +} + +sub api_endpoints_rate_limits_ip_address_edit { + my @rl_list = get_endpoint_rate_limit_label_list('ip_ranges'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + my ($rl) = grep {$$_{id} == $rl_id} @{$state{api}{endpoints}{ip_ranges}}; + $rl = _collect_endpoint_rate_limit_ip_data($rl); + $dbh->do( + "UPDATE openapi.endpoint_ip_rate_limit_map SET endpoint=?, ip_range=?, rate_limit=? WHERE id=?", undef, + $$rl{endpoint}, $$rl{ip_range}, $$rl{rate_limit}, $rl_id + ); + reload_api_endpoints(); +} + +sub api_endpoints_rate_limits_ip_address_remove { + my @rl_list = get_endpoint_rate_limit_label_list('ip_ranges'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + $dbh->do( "DELETE FROM openapi.endpoint_ip_rate_limit_map WHERE id=?", undef, $rl_id); + reload_api_endpoints(); +} + +sub _collect_endpoint_rate_limit_user_data { + my $ep = $state{api}{endpoint} || api_endpoints_load(); + my $int = $state{integrator}{id} || 0; + return unless $ep; + + my $rl = shift || { + endpoint => $$ep{operation_id}, + accessor => $int, + rate_limit => 0 + }; + + $$rl{endpoint} = prompt "Endpoint".($$rl{endpoint} ? " [$$rl{endpoint}]" :'').":", -def => $$rl{endpoint} || ''; + $$rl{accessor} = number_menu_or_argv( + "Integrator".($$rl{accessor}? " [ID:$$rl{accessor}]" : '').":", + menu_spec_from_table('openapi.integrator natural join actor.usr', 'usrname', 'id', '[Cancel]', 0), + -def => $$rl{accessor} + ); + $$rl{rate_limit} = number_menu_or_argv( + "Rate Limit".($$rl{rate_limit}? " [ID:$$rl{rate_limit}]" : '').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[Cancel]', 0), + -def => (ref $$rl{rate_limit} ? $$rl{rate_limit}{id} : 0) + ); + + $$rl{accessor} = $$rl{accessor} eq '0' ? undef : $$rl{accessor}; + $$rl{rate_limit} = $$rl{rate_limit} eq '0' ? undef : $$rl{rate_limit}; + + return $rl; +} + +sub api_endpoints_rate_limits_integrator_add { + my $rl = _collect_endpoint_rate_limit_user_data(); + $dbh->do( + "INSERT INTO openapi.endpoint_user_rate_limit_map (endpoint, accessor, rate_limit) VALUES (?,?,?)", undef, + $$rl{endpoint}, $$rl{accessor}, $$rl{rate_limit} + ); + reload_api_endpoints(); +} + +sub api_endpoints_rate_limits_integrator_edit { + my @rl_list = get_endpoint_rate_limit_label_list('user_rate_limits'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + my ($rl) = grep {$$_{id} == $rl_id} @{$state{api}{endpoints}{user_rate_limits}}; + $rl = _collect_endpoint_rate_limit_ip_data($rl); + $dbh->do( + "UPDATE openapi.endpoint_user_rate_limit_map SET endpoint=?, accessor=?, rate_limit=? WHERE id=?", undef, + $$rl{endpoint}, $$rl{accessor}, $$rl{rate_limit}, $rl_id + ); + reload_api_endpoints(); +} + +sub api_endpoints_rate_limits_integrator_remove { + my @rl_list = get_endpoint_rate_limit_label_list('user_rate_limits'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to remove', $rl_map, -def => 0 ); + return unless $rl_id; + + $dbh->do( "DELETE FROM openapi.endpoint_user_rate_limit_map WHERE id=?", undef, $rl_id); + reload_api_endpoints(); +} + + + +sub api_sets { + next_cmd('API Endpoint Set actions:' => [qw/list load unload add remove edit activate deactivate endpoints rate_limits perm_sets/]) +} + +sub _collect_set_data { + my $set = shift || { + name => '', + description => '', + active => 0, + rate_limit => { name => 'None', id => 0 } + }; + + $$set{name} = prompt "Name [$$set{name}]:", -def => $$set{name}; + $$set{description} = prompt "Description [$$set{description}]:", -def => $$set{description}; + $$set{active} = prompt "Active [".($$set{active}?'Y':'N')."]:", -yn => -def => ($$set{active} ? 'y':'n'); + $$set{rate_limit} = number_menu_or_argv( + "Rate Limit".($$set{rate_limit} ? " [$$set{rate_limit}{name}]":'').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[None]', 0), + -def => (ref $$set{rate_limit} ? $$set{rate_limit}{id} : 0) + ); + + $$set{active} = $$set{active} eq 'y' ? 1 : 0; + $$set{rate_limit} = $$set{rate_limit} eq '0' ? undef : $$set{rate_limit}; + + return $set; +} + +sub api_sets_remove { + if ($state{api}{set}{name} and yn("Continue to remove set $state{api}{set}{name}?")) { + $dbh->do( "DELETE FROM openapi.endpoint_set WHERE name = ?", undef, $state{api}{set}{name} ); + api_sets_unload(); + api_sets_list(); + } +} + +sub api_sets_add { + api_sets_unload(); + my $set = _collect_set_data(); + $dbh->do( + "INSERT INTO openapi.endpoint_set (name, description, active, rate_limit) VALUES (?,?,?,?)", undef, + $$set{name}, $$set{description}, $$set{active}, $$set{rate_limit} + ); + unshift @ARGV, $$set{name}; + api_sets_load(); +} + +sub api_sets_edit { + my $set = $state{api}{set} || api_sets_load(); + if ($set) { + my $oldname = $$set{name}; + _collect_set_data($set); + $dbh->do( + "UPDATE openapi.endpoint_set SET name = ?, description = ?, active = ?, rate_limit = ? WHERE name = ?", undef, + $$set{name}, $$set{description}, $$set{active}, $$set{rate_limit}, $oldname + ); + reload_api_sets(); + } +} + +sub api_sets_details { + $state{api_sets_details} = $state{api_sets_details} ? 0 : 1; + print ' ! API Endpoint Set Detail Dispaly: '. ($state{api_sets_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub api_sets_list { + print "API Endpoint Sets:\n" + . "==============\n" if !$from_command; + print join( + "\n", + map { + "$$_{name}: $$_{description} " . ($$_{active} ? '(Active)' : '(Inactive)') + } $dbh->selectall_array('SELECT * FROM openapi.endpoint_set ORDER BY name',{Slice => {}}) + ) . "\n"; +} + +sub api_sets_activate { + my $enabled = shift || 'TRUE'; + my $name = $state{api}{set}{name} || value_or_argv('Endpoint Set Name:'); + if ($name ne '') { + $dbh->do("UPDATE openapi.endpoint_set SET active = $enabled WHERE name = ?", undef, $name); + unshift @ARGV, $name; + api_set_load(); + } +} +sub api_sets_deactivate { api_sets_activate('FALSE') } + +sub api_sets_perm_sets { + return unless $state{api}{set}{name} || api_sets_load(); + next_cmd('API Endpoint Set Permission Set actions:' => [qw/list add remove/]) +} + +sub api_sets_perm_sets_list { + print "Permission Sets for Endpoint Set\n" + . "============================\n" if !$from_command; + print join("\n", map { $$_{name} .': '. join(', ', map {$$_{code}} @{$$_{perms}}) } @{$state{api}{set}{perm_sets}})."\n"; +} + +sub api_sets_perm_sets_remove { + return unless @{$state{api}{set}{perm_sets}}; + if (my $s = number_menu_or_argv('Permission Set:', { map {$$_{name} => $$_{id}} @{$state{api}{set}{perm_sets}} })) { + $dbh->do( "DELETE FROM openapi.endpoint_set_perm_set_map WHERE endpoint_set = ? AND perm_set = ?", undef, $state{api}{set}{name}, $s ); + reload_api_sets(); + } +} + +sub api_sets_perm_sets_add { + my $options = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.perm_set WHERE id NOT IN (SELECT perm_set FROM openapi.endpoint_set_perm_set_map WHERE endpoint_set = ?)', + { Slice => {} }, $state{api}{set}{name} + ); + return unless @$options; + if (my $s = number_menu_or_argv('API Set:', { map {$$_{name} => $$_{id}} @$options })) { + $dbh->do( + "INSERT INTO openapi.endpoint_set_perm_set_map (endpoint_set, perm_set) VALUES (?,?)", + undef, $state{api}{set}{name}, $s + ); + reload_api_sets(); + } +} + + +sub api_sets_endpoints { + return unless $state{api}{set}{name} || api_sets_load(); + next_cmd('API Sets Assigned Endpoint actions:' => [qw/list add remove/]) +} + +sub api_sets_endpoints_list { + print "API Endpoints for Set\n" + . "=====================\n" if !$from_command; + print join("\n", @{$state{api}{set}{endpoints}})."\n"; +} + +sub api_sets_endpoints_remove { + return unless @{$state{api}{set}{endpoints}}; + if (my $s = value_or_argv('API Endpoint:', -comp => $state{api}{set}{endpoints})) { + if (yn("Remove endpoint $s from set $state{api}{set}{name}?")) { + $dbh->do( "DELETE FROM openapi.endpoint_set_endpoint_map WHERE endpoint = ? AND endpoint_set = ?", undef, $s, $state{api}{set}{name} ); + reload_api_sets(); + } + } +} + +sub api_sets_endpoints_add { + my $options = $dbh->selectcol_arrayref( + 'SELECT s.operation_id FROM openapi.endpoint s WHERE operation_id NOT IN (SELECT endpoint FROM openapi.endpoint_set_endpoint_map WHERE endpoint_set = ?)', + undef, $state{api}{set}{name} + ); + return unless @$options; + if (my $s = value_or_argv('API Endpoint', -comp => $options)) { + if (yn("Add endpoint $s to set $state{api}{set}{name}?")) { + $dbh->do( + "INSERT INTO openapi.endpoint_set_endpoint_map (endpoint, endpoint_set) VALUES (?,?)", + undef, $s, $state{api}{set}{name} + ); + reload_api_sets(); + } + } +} + +sub api_sets_rate_limits { + return unless $state{api}{set}{name} || api_sets_load(); + next_cmd('API Set rate limit types:' => [qw/ip_address integrator/]) +} + +sub api_sets_rate_limits_ip_address { + next_cmd('API Set IP Address rate limit commands:' => [qw/list add edit remove/]) +} + +sub api_sets_rate_limits_ip_address_list { + return api_sets_rate_limits_list('ip_ranges'); +} + +sub api_sets_rate_limits_integrator { + next_cmd('API Set IP Address rate limit commands:' => [qw/list add edit remove/]) +} + +sub api_sets_rate_limits_integrator_list { + return api_sets_rate_limits_list('user_rate_limits'); +} + +sub get_set_rate_limit_label_list { + my $type = shift; + my @pile; + for my $rl ( @{$state{api}{set}{$type}} ) { + my $def = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.rate_limit_definition WHERE id = ?', + undef, $$rl{rate_limit} + ); + + my $limited = $type eq 'user_rate_limits' ? $dbh->selectrow_hashref( + 'SELECT usrname FROM actor.usr WHERE id = ?', + undef, $$rl{accessor} + )->{usrname} : $$rl{ip_range}; + + push @pile, {"$limited limited to $$def{limit_count} per $$def{limit_interval}" => $$rl{id}}; + } + return @pile; +} + +sub api_sets_rate_limits_list { + my $type = shift; + my $type_label = $type eq 'user_rate_limits' ? 'User-specific' : 'IP Range'; + + print "$type_label Endpoint Set Rate Limits for $state{api}{set}{name}\n" + . "===================================================\n" if !$from_command; + + print join("\n", map { + my $label = (keys(%$_))[0]; + $label = "$state{api}{set}{name}: $label" if $from_command; + $label; + } get_set_rate_limit_label_list($type))."\n"; +} + +sub _collect_set_rate_limit_ip_data { + my $set = $state{api}{set} || api_sets_load(); + return unless $set; + + my $rl = shift || { + endpoint_set => $$set{name}, + ip_range => '', + rate_limit => undef + }; + + $$rl{endpoint_set} = prompt "Endpoint Set".($$rl{endpoint_set} ? " [$$rl{endpoint_set}]" :'').":", -def => $$rl{endpoint_set} || ''; + $$rl{ip_range} = prompt "CIDR IP Range".($$rl{ip_range} ? " [$$rl{ip_range}]" :'').":", -def => $$rl{ip_range} || ''; + $$rl{rate_limit} = number_menu_or_argv( + "Rate Limit".($$rl{rate_limit}? " [$$rl{rate_limit}{name}]" : '').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[Cancel]', 0), + -def => $$rl{rate_limit} || 0 + ); + + $$rl{rate_limit} = $$rl{rate_limit} eq '0' ? undef : $$rl{rate_limit}; + + return $rl; +} + +sub api_sets_rate_limits_ip_address_add { + my $rl = _collect_set_rate_limit_ip_data(); + return unless $$rl{rate_limit}; + + $dbh->do( + "INSERT INTO openapi.endpoint_set_ip_rate_limit_map (endpoint_set, ip_range, rate_limit) VALUES (?,?,?)", undef, + $$rl{endpoint_set}, $$rl{ip_range}, $$rl{rate_limit} + ); + reload_api_sets(); +} + +sub api_sets_rate_limits_ip_address_edit { + my @rl_list = get_set_rate_limit_label_list('ip_ranges'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + my ($rl) = grep {$$_{id} == $rl_id} @{$state{api}{set}{ip_ranges}}; + $rl = _collect_set_rate_limit_ip_data($rl); + $dbh->do( + "UPDATE openapi.endpoint_set_ip_rate_limit_map SET endpoint_set=?, ip_range=?, rate_limit=? WHERE id=?", undef, + $$rl{endpoint_set}, $$rl{ip_range}, $$rl{rate_limit}, $rl_id + ); + reload_api_sets(); +} + +sub api_sets_rate_limits_ip_address_remove { + my @rl_list = get_set_rate_limit_label_list('ip_ranges'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + $dbh->do( "DELETE FROM openapi.endpoint_set_ip_rate_limit_map WHERE id=?", undef, $rl_id); + reload_api_sets(); +} + +sub _collect_set_rate_limit_user_data { + my $set = $state{api}{set} || api_sets_load(); + my $int = $state{integrator}{id} || 0; + return unless $set; + + my $rl = shift || { + endpoint_set => $$set{name}, + accessor => $int, + rate_limit => 0 + }; + + $$rl{endpoint_set} = prompt "Endpoint Set".($$rl{endpoint_set} ? " [$$rl{endpoint_set}]" :'').":", -def => $$rl{endpoint_set} || ''; + $$rl{accessor} = number_menu_or_argv( + "Integrator".($$rl{accessor}? " [ID:$$rl{accessor}]" : '').":", + menu_spec_from_table('openapi.integrator natural join actor.usr', 'usrname', 'id', '[Cancel]', 0), + -def => $$rl{accessor} || 0 + ); + $$rl{rate_limit} = number_menu_or_argv( + "Rate Limit".($$rl{rate_limit}? " [ID:$$rl{rate_limit}]" : '').":", + menu_spec_from_table('openapi.rate_limit_definition', 'name', 'id', '[Cancel]', 0), + -def => $$rl{rate_limit} || 0 + ); + + $$rl{accessor} = $$rl{accessor} eq '0' ? undef : $$rl{accessor}; + $$rl{rate_limit} = $$rl{rate_limit} eq '0' ? undef : $$rl{rate_limit}; + + return $rl; +} + +sub api_sets_rate_limits_integrator_add { + my $rl = _collect_set_rate_limit_user_data(); + return unless $$rl{rate_limit} and $$rl{accessor}; + $dbh->do( + "INSERT INTO openapi.endpoint_set_user_rate_limit_map (endpoint_set, accessor, rate_limit) VALUES (?,?,?)", undef, + $$rl{endpoint_set}, $$rl{accessor}, $$rl{rate_limit} + ); + reload_api_sets(); +} + +sub api_sets_rate_limits_integrator_edit { + my @rl_list = get_set_rate_limit_label_list('user_rate_limits'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to edit:', $rl_map, -def => 0 ); + return unless $rl_id; + + my ($rl) = grep {$$_{id} == $rl_id} @{$state{api}{set}{user_rate_limits}}; + $rl = _collect_set_rate_limit_user_data($rl); + $dbh->do( + "UPDATE openapi.endpoint_set_user_rate_limit_map SET endpoint_set=?, accessor=?, rate_limit=? WHERE id=?", undef, + $$rl{endpoint_set}, $$rl{accessor}, $$rl{rate_limit}, $rl_id + ); + reload_api_sets(); +} + +sub api_sets_rate_limits_integrator_remove { + my @rl_list = get_set_rate_limit_label_list('user_rate_limits'); + my $rl_map = { map {%$_} @rl_list }; + $$rl_map{' [Cancel]'} = 0; + my $rl_id = number_menu_or_argv( 'Rate Limit to remove', $rl_map, -def => 0 ); + return unless $rl_id; + + $dbh->do( "DELETE FROM openapi.endpoint_set_user_rate_limit_map WHERE id=?", undef, $rl_id); + reload_api_sets(); +} + + +sub integrator { + next_cmd('Integrator actions:' => [qw/list load unload add remove enable disable password + details permissions groups global_property_whitelist + global_property_blacklist endpoint_property_whitelist + endpoint_property_blacklist/]); +} + +sub integrator_global_property_whitelist { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Global Fieldmapper property whitelist actions:' => [qw/list set remove/]); +} + +sub integrator_global_property_blacklist { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Global Fieldmapper property blacklist actions:' => [qw/list set remove/]); +} + +sub integrator_global_property_whitelist_list { + return show_global_integrator_property_filter('whitelist'); +} + +sub integrator_global_property_blacklist_list { + return show_global_integrator_property_filter('blacklist'); +} + +sub integrator_endpoint_property_whitelist_list { + return show_endpoint_integrator_property_filter('whitelist'); +} + +sub integrator_endpoint_property_blacklist_list { + return show_endpoint_integrator_property_filter('blacklist'); +} + +sub show_global_integrator_property_filter { + my $type = shift; + + my $setting_name = "REST.api.${type}_properties"; + + return print_filter_list(Global => $type => [ grep { $$_{name} eq $setting_name } @{$state{integrator}{filter_settings}} ]); + +} + +sub show_endpoint_integrator_property_filter { + my $type = shift; + my $endpoint = shift || ''; + + my $setting_name = "REST.api.${type}_properties.$endpoint"; + + return print_filter_list(Endpoint => $type => [ grep { $$_{name} =~ /^$setting_name/ } @{$state{integrator}{filter_settings}} ]); +} + +sub print_filter_list { + my $scope = shift; + my $type = shift; + my $settings = shift || []; + + print "$scope $type properties\n" + . "==================================\n" if !$from_command; + print ''.($scope eq 'Endpoint' ? +(split '\.', $$_{name})[-1] . ': ' : '')."$$_{value}\n" for @$settings; +} + +sub integrator_global_property_whitelist_set { + return set_property_filter('whitelist'); +} + +sub integrator_global_property_blacklist_set { + return set_property_filter('blacklist'); +} + +sub integrator_global_property_whitelist_remove { + return remove_property_filter('whitelist'); +} + +sub integrator_global_property_blacklist_remove { + return remove_property_filter('blacklist'); +} + +sub find_property_filter_value { + my $name = shift; + my $setting = [grep { $$_{name} eq $name } @{$state{integrator}{filter_settings}}]->[0]; + return $$setting{value} if $setting; + return undef; +} + +sub set_property_filter { + my $type = shift; + my $endpoint = shift; + + my $name = "REST.api.${type}_properties"; + $name .= ".$endpoint" if $endpoint; + + my $current_value = find_property_filter_value($name); + + if (my $id = $state{integrator}{id} || integrator_load("Integrator username adjust ${type}ed properties:")) { + print "Current value of $name: $current_value\n" if $current_value; + my $new_value = value_or_argv("Comma separated list of properties to $type, [Enter] to leave unchanged:"); + if ($new_value) { + $new_value =~ s/"//g; + $new_value = '"'.$new_value.'"'; + + # auto-vivicate the user setting type for endpoint-specific ones + $dbh->do( + 'INSERT INTO config.usr_setting_type (name,label,grp) VALUES (?,?,?) ON CONFLICT DO NOTHING;', undef, + $name,"OpenAPI Fieldmapper property $type for endpoint $endpoint",'openapi' + ) if $endpoint; + + $dbh->do( + 'INSERT INTO actor.usr_setting (usr,name,value) VALUES (?,?,?) ON CONFLICT (usr,name) DO UPDATE SET value = ?;', undef, + $id,$name,$new_value,$new_value + ); + reload_integrator(); + } + } +} + +sub remove_property_filter { + my $type = shift; + my $endpoint = shift; + + my $name = "REST.api.${type}_properties"; + $name .= ".$endpoint" if $endpoint; + + my $current_value = find_property_filter_value($name); + + if (my $id = $state{integrator}{id} || integrator_load("Integrator username to remove ${type}ed properties from:")->{id}) { + print "Current value of $name: $current_value\n" if $current_value; + if (yn("Continue to remove $type in setting $name for [$state{integrator}{usrname}]?")) { + $dbh->do('DELETE FROM actor.usr_setting WHERE usr = ? AND name = ?;', undef, $id, $name); + reload_integrator(); + } + } +} + +sub existing_endpoint_property_filter_endpoints { + return [] if !$state{integrator}{id}; + return [ + sort { + $a cmp $b + } map { + s/^REST\.api\.\w+?_properties\.//; $_; + } grep { + /^REST\.api\.\w+?_properties\./ + } map { + $$_{name} + } @{$state{integrator}{filter_settings}} + ]; +} + +sub integrator_endpoint_property_whitelist { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Endpoint-specific Fieldmapper property whitelist actions:' => [qw/list add edit remove/]); +} + +sub integrator_endpoint_property_blacklist { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Endpoint-specific Fieldmapper property blacklist actions:' => [qw/list add edit remove/]); +} + +sub integrator_endpoint_property_whitelist_add { + return integrator_endpoint_property_filter_change(add => 'whitelist'); +} + +sub integrator_endpoint_property_blacklist_add { + return integrator_endpoint_property_filter_change(add => 'blacklist'); +} + +sub integrator_endpoint_property_whitelist_edit { + return integrator_endpoint_property_filter_change(edit => 'whitelist'); +} + +sub integrator_endpoint_property_blacklist_edit { + return integrator_endpoint_property_filter_change(edit => 'blacklist'); +} + +sub integrator_endpoint_property_whitelist_remove { + return integrator_endpoint_property_filter_change(remove => 'whitelist'); +} + +sub integrator_endpoint_property_blacklist_remove { + return integrator_endpoint_property_filter_change(remove => 'blacklist'); +} + +sub integrator_endpoint_property_filter_change { + my $action = shift; + my $type = shift; + + if ($state{integrator}{usrname} || integrator_load("Integrator username to $action ${type}ed properties for:")) { + my $existing_endpoints = existing_endpoint_property_filter_endpoints(); + if ($action eq 'add' or @$existing_endpoints) { + my $where = @$existing_endpoints ? "WHERE operation_id ".($action eq 'add' ? 'NOT' : '')." IN ('".join("','", @$existing_endpoints)."')" : ''; + my $new_endpoint = number_menu_or_argv( + "Endpoint to $action $type:", + menu_spec_from_table("openapi.endpoint $where","operation_id || ': ' || http_method || ' ' || path",'operation_id','[Cancel]',0) + ); + if ($new_endpoint ne '0') { + if ($action eq 'remove') { + remove_property_filter($type, $new_endpoint); + } else { + set_property_filter($type, $new_endpoint); + } + } + } + } +} + +sub integrator_list { + print "Integrators:\n" + . "============\n" if !$from_command; + print join( + "\n", + map { + "$$_{usrname}: " . ($$_{enabled} ? 'Enabled' : 'Disabled') + } $dbh->selectall_array('SELECT u.usrname, i.enabled FROM actor.usr u JOIN openapi.integrator i USING (id) WHERE NOT u.deleted ORDER BY 1',{Slice => {}}) + ) . "\n"; +} + +sub integrator_password { + if ($state{integrator} or integrator_load()) { + my $pw = pw_value("New API password for [$state{integrator}{usrname}]:"); + my $pw2 = pw_value("Confirm new API password for [$state{integrator}{usrname}]:"); + + if ($pw ne $pw2) { + print " !! passwords don't match \n"; + return; + } + + if ($pw ne '' and yn("Continue to (re)set password for [$state{integrator}{usrname}]?")) { + my $salt = $dbh->selectcol_arrayref("SELECT actor.create_salt('api')")->[0]; + $dbh->do("SELECT actor.set_passwd(?,'api',?,?)", undef, $state{integrator}{id}, md5_hex($salt . md5_hex($pw)), $salt); + } + } else { + print " !! Load integrator before (re)setting password\n"; + } +} + +sub integrator_add { + my $usrname = value_or_argv('New Integrator username:'); + if ($usrname ne '') { + $dbh->do('INSERT INTO openapi.integrator (id) SELECT id FROM actor.usr WHERE usrname = ?', undef, $usrname); + unshift @ARGV, $usrname; + integrator_load(); + } +} + +sub integrator_remove { + my $usrname = $state{integrator}{usrname} || integrator_load('Integrator username to remove:')->{usrname}; + if ($usrname ne '' and yn("Continue to remove integrator $usrname?")) { + $dbh->do('DELETE FROM openapi.integrator WHERE id IN (SELECT id FROM actor.usr WHERE usrname = ?)', undef, $usrname); + } +} + +sub integrator_enable { + my $enabled = shift || 'TRUE'; + my $usrname = $state{integrator}{usrname} || value_or_argv('Integrator username:'); + if ($usrname ne '') { + $dbh->do("UPDATE openapi.integrator SET enabled = $enabled WHERE id IN (SELECT id FROM actor.usr WHERE usrname = ?)", undef, $usrname); + unshift @ARGV, $usrname; + integrator_load(); + } +} +sub integrator_disable { integrator_enable('FALSE') } + +sub integrator_permissions { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Integrator permission actions:' => [qw/list add remove/]) +} + +sub integrator_permissions_list { + print "Permissions:\n" + . "============\n" if !$from_command; + print join("\n", map {$$_{code}} @{$state{integrator}{user_perms}}) . "\n\n"; +} + +sub integrator_permissions_add { + my $new_perm = list_or_argv('New Permission:' => $dbh->selectcol_arrayref('SELECT code FROM permission.perm_list')); + if ($new_perm ne '') { + my $new_depth = number_menu_or_argv('Permission Depth:' => depth_menu_spec()); + if ($new_depth and yn("Add permission [$new_perm]?")) { + $new_perm = $dbh->selectcol_arrayref('SELECT id FROM permission.perm_list WHERE code = ?', undef, $new_perm)->[0]; + $dbh->do('INSERT INTO permission.usr_perm_map (usr,perm,depth) VALUES (?,?,?)', undef, $state{integrator}{id}, $new_perm, $new_depth); + reload_integrator(); + } + } +} + +sub integrator_permissions_remove { + if (@{$state{integrator}{user_perms}}) { + my $perm = list_or_argv('Permission:' => [map {$$_{code}} @{$state{integrator}{user_perms}}]); + if ($perm ne '' and yn("Remove permission [$perm]?")) { + $perm = $dbh->selectcol_arrayref('SELECT id FROM permission.perm_list WHERE code = ?', undef, $perm)->[0]; + $dbh->do('DELETE FROM permission.usr_perm_map WHERE usr = ? AND perm = ?', undef, $state{integrator}{id}, $perm) if ($perm); + reload_integrator(); + } + } +} + +sub integrator_groups { + my $usrname = $state{integrator}{usrname} || integrator_load()->{usrname}; + next_cmd('Integrator secondary group actions:' => [qw/list add remove/]) +} + +sub integrator_groups_list { + print "Groups:\n" + . "=======\n" if !$from_command; + print join("\n", map {$$_{name}} @{$state{integrator}{secondary_groups}}) . "\n\n"; +} + +sub integrator_groups_add { + my $new_group = number_menu_or_argv('New Group:' => menu_spec_from_table('permission.grp_tree','name','id','[Cancel]' => 0)); + my ($new_group_name) = $dbh->selectrow_array('SELECT name FROM permission.grp_tree WHERE id = ?', undef, $new_group); + if ($new_group ne '0' and yn("Add Secondary Group [$new_group_name]?")) { + $dbh->do('INSERT INTO permission.usr_grp_map (usr,grp) VALUES (?,?)', undef, $state{integrator}{id}, $new_group); + reload_integrator(); + } +} + +sub integrator_groups_remove { + if (@{$state{integrator}{secondary_groups}}) { + my $group = number_menu_or_argv('Group:' => {' [Cancel]' => 0, map { $$_{name} => $$_{id} } @{$state{integrator}{secondary_groups}}}); + my ($group_name) = $dbh->selectrow_array('SELECT name FROM permission.grp_tree WHERE id = ?', undef, $group); + if ($group ne '0' and yn("Remove Secondary Group [$group_name]?")) { + $dbh->do('DELETE FROM permission.usr_grp_map WHERE usr = ? AND grp = ?', undef, $state{integrator}{id}, $group); + reload_integrator(); + } + } +} + +sub integrator_details { + $state{integrator_details} = $state{integrator_details} ? 0 : 1; + print ' ! Integrator Detail Dispaly: '. ($state{integrator_details} ? 'On' : 'Off'). "\n" if !$from_command; +} + +sub integrator_unload { + delete $state{integrator}; +} + +sub reload_integrator { + if ($state{integrator}{usrname}) { + unshift @ARGV, $state{integrator}{usrname}; + integrator_load(); + } +} + +sub integrator_load { + my $prompt = shift || 'Integrator username:'; + $state{integrator} = $dbh->selectrow_hashref( + 'SELECT u.*, i.enabled AS api_enabled FROM actor.usr u JOIN openapi.integrator i USING (id) WHERE NOT u.deleted AND u.usrname = ?', + undef, value_or_argv($prompt) + ); + + if (!$state{integrator}) { + print " ! Integrator account not found\n"; + return undef; + } + + $state{integrator}{profile} = $dbh->selectrow_hashref( + 'SELECT * FROM permission.grp_tree WHERE id = ?', + undef, $state{integrator}{profile} + ); + + $state{integrator}{secondary_groups} = $dbh->selectall_arrayref( + 'SELECT g.* FROM permission.grp_tree g JOIN permission.usr_grp_map m ON (m.grp = g.id) WHERE m.usr = ?', + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{filter_settings} = $dbh->selectall_arrayref( + "SELECT * FROM actor.usr_setting WHERE name like 'REST.api.%properties%' AND usr = ? ORDER BY name", + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{endpoint_rate_limits} = $dbh->selectall_arrayref( + 'SELECT m.endpoint, r.* FROM openapi.endpoint_user_rate_limit_map m JOIN openapi.rate_limit_definition r ON (r.id = m.rate_limit) WHERE m.accessor = ?', + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{endpoint_set_rate_limits} = $dbh->selectall_arrayref( + 'SELECT m.endpoint_set, r.* FROM openapi.endpoint_set_user_rate_limit_map m JOIN openapi.rate_limit_definition r ON (r.id = m.rate_limit) WHERE m.accessor = ?', + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{login_attempt_summary} = $dbh->selectrow_hashref( + 'SELECT MIN(attempt_time) AS first_attempt,'. + ' MAX(attempt_time) AS last_attempt,'. + ' SUM((token IS NOT NULL)::INT) AS success,'. + " SUM((NULLIF(token,'') IS NULL)::INT) AS failure". + ' FROM openapi.authen_attempt_log'. + ' WHERE cred_user = ?'. + ' GROUP BY cred_user', + undef, $state{integrator}{usrname} + ); + + $state{integrator}{endpoint_access_summary} = $dbh->selectall_arrayref( + 'SELECT endpoint,'. + ' MIN(attempt_time) AS first_attempt,'. + ' MAX(attempt_time) AS last_attempt,'. + ' SUM(allowed::INT) AS success,'. + ' SUM((allowed IS FALSE)::INT) AS failure'. + ' FROM openapi.endpoint_access_attempt_log'. + ' WHERE accessor = ?'. + ' GROUP BY endpoint', + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{user_perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM permission.perm_list p JOIN permission.usr_perm_map m ON (m.perm = p.id) WHERE m.usr = ?', + { Slice => {} }, $state{integrator}{id} + ); + + $state{integrator}{card} = $dbh->selectrow_hashref( + 'SELECT * FROM actor.card WHERE id = ?', + undef, $state{integrator}{card} + ) if $state{integrator}{card}; + + return $state{integrator}; +} + +sub api_endpoints_unload { + delete $state{api}{endpoint}; +} + +sub reload_api_endpoints { + if ($state{api}{endpoint}{operation_id}) { + unshift @ARGV, $state{api}{endpoint}{operation_id}; + api_endpoints_load(); + } +} + +sub api_endpoints_load { + my $prompt = shift || 'API Endpoint Operation Id:'; + $state{api}{endpoint} = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.endpoint WHERE operation_id = ?', + undef, number_menu_or_argv( + $prompt => menu_spec_from_table('openapi.endpoint',"http_method || ' ' || path",'operation_id','[Cancel]',0) + ) + ); + + if (!$state{api}{endpoint}) { + print " ! API Endpoint not found\n"; + return undef; + } + + $state{api}{endpoint}{ip_ranges} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_ip_rate_limit_map WHERE endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{user_rate_limits} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_user_rate_limit_map WHERE endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{rate_limit} = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.rate_limit_definition WHERE id = ?', + undef, $state{api}{endpoint}{rate_limit} + ) if $state{api}{endpoint}{rate_limit}; + + $state{api}{endpoint}{params} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_param WHERE endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{responses} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_response WHERE endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{sets} = $dbh->selectcol_arrayref( + 'SELECT endpoint_set FROM openapi.endpoint_set_endpoint_map WHERE endpoint = ?', + undef, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM permission.perm_list p JOIN openapi.endpoint_perm_map m ON (m.perm = p.id) WHERE m.endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + $state{api}{endpoint}{perm_sets} = $dbh->selectall_arrayref( + 'SELECT s.* FROM openapi.perm_set s JOIN openapi.endpoint_perm_set_map m ON (m.perm_set = s.id) WHERE m.endpoint = ?', + { Slice => {} }, $state{api}{endpoint}{operation_id} + ); + + for my $ps ( @{$state{api}{endpoint}{perm_sets}} ) { + $$ps{perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM permission.perm_list p JOIN openapi.perm_set_perm_map m ON (m.perm = p.id) WHERE m.perm_set = ?', + { Slice => {} }, $$ps{id} + ); + } + + return $state{api}{endpoint}; +} + +sub api_sets_unload { + delete $state{api}{set}; +} + +sub reload_api_sets { + if ($state{api}{set}{name}) { + unshift @ARGV, $state{api}{set}{name}; + api_sets_load(); + } +} + +sub api_sets_load { + my $prompt = shift || 'API Endpoint Set Name:'; + $state{api}{set} = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.endpoint_set WHERE name = ?', + undef, number_menu_or_argv( + $prompt => menu_spec_from_table('openapi.endpoint_set',"description",'name','[Cancel]',0) + ) + ); + + if (!$state{api}{set}) { + print " ! API Endpoint Set not found\n"; + return undef; + } + + $state{api}{set}{user_rate_limits} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_set_user_rate_limit_map WHERE endpoint_set = ?', + { Slice => {} }, $state{api}{set}{name} + ); + + $state{api}{set}{ip_ranges} = $dbh->selectall_arrayref( + 'SELECT * FROM openapi.endpoint_set_ip_rate_limit_map WHERE endpoint_set = ?', + { Slice => {} }, $state{api}{set}{name} + ); + + $state{api}{set}{rate_limit} = $dbh->selectrow_hashref( + 'SELECT * FROM openapi.rate_limit_definition WHERE id = ?', + undef, $state{api}{set}{rate_limit} + ) if $state{api}{set}{rate_limit}; + + $state{api}{set}{endpoints} = $dbh->selectcol_arrayref( + 'SELECT endpoint FROM openapi.endpoint_set_endpoint_map WHERE endpoint_set = ?', + undef, $state{api}{set}{name} + ); + + $state{api}{set}{perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM permission.perm_list p JOIN openapi.endpoint_set_perm_map m ON (m.perm = p.id) WHERE m.endpoint_set = ?', + { Slice => {} }, $state{api}{set}{name} + ); + + $state{api}{set}{perm_sets} = $dbh->selectall_arrayref( + 'SELECT s.* FROM openapi.perm_set s JOIN openapi.endpoint_set_perm_set_map m ON (m.perm_set = s.id) WHERE m.endpoint_set = ?', + { Slice => {} }, $state{api}{set}{name} + ); + + for my $ps ( @{$state{api}{set}{perm_sets}} ) { + $$ps{perms} = $dbh->selectall_arrayref( + 'SELECT p.* FROM permission.perm_list p JOIN openapi.perm_set_perm_map m ON (m.perm = p.id) WHERE m.perm_set = ?', + { Slice => {} }, $$ps{id} + ); + } + + return $state{api}{set}; +} + diff --git a/Open-ILS/src/support-scripts/openapi_server b/Open-ILS/src/support-scripts/openapi_server new file mode 100755 index 0000000000..a7e2d0567e --- /dev/null +++ b/Open-ILS/src/support-scripts/openapi_server @@ -0,0 +1,1016 @@ +#!/usr/bin/perl + +# --------------------------------------------------------------- +# Copyright (C) 2024 Equinox Open Library Initiative, Inc. +# Mike Rylander <mrylander@gmail.com> +# Galen Charlton <gmc@equinoxOLI.org> + +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# --------------------------------------------------------------- + +# Add an nginx config block to the https section: +# +# location /openapi3 { +# proxy_pass https://localhost:8080; +# proxy_set_header Host $host; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_read_timeout 300s; +# } +# +# +# And start it up like this: +# +# /openils/bin/openapi_server prefork -m production -l https://localhost:8080 +# +# +# For server option details, see: https://docs.mojolicious.org/Mojolicious/Guides/Cookbook#DEPLOYMENT +# + + +use strict; +use warnings; + +my $U = "OpenILS::Application::AppUtils"; + +package OpenILS::APIServer; + +use MIME::Base64; +use Scalar::Util qw/blessed/; +use Types::Serialiser; +use Mojolicious::Lite; +use OpenSRF::System; +use OpenSRF::AppSession; +use OpenSRF::Utils::SettingsClient; +use OpenSRF::EX qw(:try); +use OpenILS::Utils::Fieldmapper; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor q/new_editor/; +use OpenSRF::Utils::Logger q/$logger/; +use Data::Dumper; + +#------------------------------------------------- +# globals and setup + +my $osrf_config = '/openils/conf/opensrf_core.xml'; +my $rev_proxy_levels = $ENV{EG_REV_PROXY_LEVELS} // 1; + +OpenSRF::System->bootstrap_client(config_file => $osrf_config); + +Fieldmapper->import( + IDL => OpenSRF::Utils::SettingsClient->new->config_value("IDL") +); + +OpenILS::Utils::CStoreEditor->init; + +#------------------------------------------------- +# Build the OpenAPI 3 config blob + +my $config = { + openapi => "3.0.3", + info => { + description => "RESTful API for the Evergreen ILS", + version => "1.0.0", + title => "Evergreen API", + license => { + name => "GNU Public License 2.0+" + } + }, + servers => [ + { url => "/openapi3/v1" } + ], + tags => [], + components => { + securitySchemes => { + basicAuth => { # /only/ used to get a token, handler does the actual auth and the plugin sub is just a passthrough + type => "http", + scheme => "basic" + }, + passthroughUser => { # /only/ used to get a token, handler does the actual auth and the plugin sub is just a passthrough + type => "apiKey", + in => "query", + name => "u" + }, + passthroughPass => { # /only/ used to get a token, handler does the actual auth and the plugin sub is just a passthrough + type => "apiKey", + in => "query", + name => "p" + }, + bearerAuth => { + type => "http", + scheme => "bearer" + }, + cookieAuth => { + type => "apiKey", + in => "cookie", + name => "eg.api.token" + }, + paramAuth => { + type => "apiKey", + in => "query", + name => "ses" + } + }, + schemas => generate_schemas() + } +}; + +# gather tags (endpoint sets)... +add_endpoint_sets(); + +# ... and, finally, gather the registered paths ... +add_dynamic_endpoints(); + +#------------------------------------------------- +# Finally, let the openapi plugin do it's thing + +plugin OpenAPI => { + #skip_validating_specification => 1, + spec => JSON::Validator::Schema::OpenAPIv3::Evergreen->new($config), + security => { + basicAuth => sub { return $_[3]->($_[0]) }, # basic auth is only used for initial login + bearerAuth => sub { return securityCheck(bearerAuth => @_) }, + cookieAuth => sub { return securityCheck(cookieAuth => @_) }, + paramAuth => sub { return securityCheck(paramAuth => @_) } + }, +}; + +# We should be behind a reverse proxy, probably nginx. Set the +# env var EG_REV_PROXY_LEVELS to the depth of the reverse proxy +# stack that YOU control. Theoretically, that could even be "0" +# if you were to expose this API server directly to clients. +plugin ForwardedFor => { levels => $rev_proxy_levels }; + +#------------------------------------------------- +# Almost ready to go... + +app->hook(after_dispatch => sub { log_authenticated_dispatch_event(@_) }); +app->hook(after_dispatch => sub { $_[0]->res->headers->remove('Server') }); + +# XXX If we want to use session cookies we'll need to come +# up with a better way to set secrets(). +app->secrets(scalar time)->start; + + + + + + + +#------------------------------------------------- +# Support functions + +sub add_dynamic_endpoints { + my $paths = new_editor()->json_query({ + distinct => 1, + select => { oep => ['path'] }, + from => 'oep', + where => { active => 't' } + }); + + # ... and register all their HTTP method handlers ... + for my $path (map {$$_{path}} @$paths) { + $logger->info("Loading path configuration for: $path"); + my $path_config = fetch_path_config($path); + add_path($path => $_ => @{$$path_config{$_}}) for keys %$path_config; + } +} + +sub add_endpoint_sets { + my $tags = new_editor()->search_openapi_endpoint_set({ active => 't' }); + $logger->info( + "Loading endpoint sets (OpenAPI tags): ".join(' ', map { $_->name} @$tags) + ) if @$tags; + + $$config{tags} ||= []; + for my $tag (@$tags) { + if (!grep {$$_{name} eq $tag->name} @{$$config{tags}}) { + push @{$$config{tags}}, {name => $tag->name, description => $tag->description}; + } + } +} + +sub _build_handler_params { + my $c = shift; + my $pdef = shift || []; + + my @params; + for my $part (@$pdef) { + if ($part =~ /^(["'])(.*?)\1$/) { # a quoted literal? just pass it along + push @params, $2; + next; + } + + my $c_method = 'stash'; # assume stash for unqualified part names + my @subparts = split /\./, $part; + + # useful prefixes: stash [every_]param [every_|signed_]cookie req session + if (scalar(@subparts) > 1) { # has a .? + $c_method = shift @subparts; + } + + # req needs a little shoving around to be useful. the others + # (generally) are generalized accessor/mutator objects, req + # is an object with methods we want to call. + my $context_obj = $c; + if ($c_method eq 'req') { + $c_method = shift @subparts; + $context_obj = $c->req; + } + + push @params, $context_obj->$c_method(@subparts); + } + + return @params; +} + +# returns renderer handler name, sets $c->stash('eg_req_resolved_content_type'), and might set $c->stash('format') +sub resolve_requested_render_type { + my $c = shift; + my $endpoint = shift; + + my $validated = 0; + my @resp_types = qw#application/json#; + + if (!ref($endpoint)) { + $validated = $endpoint; + } else { + $validated = grep { $U->is_true($_->validate) and $_->status == 200 } @{$endpoint->responses}; + @resp_types = map { $_->content_type } @{$endpoint->responses}; + } + + my %default_tokens; + if (my $accept_tokens = $c->req->headers->header('X-EG-OpenAPI-Options')) { + $accept_tokens =~ s/^\s+|\s+$//g; + $c->app->log->trace("Received options [$accept_tokens] via header [X-EG-OpenAPI-Options] operationId: ".$c->stash('operationId')); + + my @parts = split ';', $accept_tokens; + for my $part (@parts) { + my ($token_key, $token_value) = split '=', $part; + if (defined($token_key) and defined($token_value)) { + $token_key =~ s/^\s+|\s+$//g; + $token_value =~ s/^\s+|\s+$//g; + $default_tokens{$token_key} = $token_value; + $c->app->log->trace("Adding default Accept header option [$token_key] with value [$token_value] via header [X-EG-OpenAPI-Options] for operationId: ".$c->stash('operationId')); + } + } + } + + my $hdr = $c->req->headers->accept || ''; + $hdr =~ s/^\s+|\s+$//g; + + unless ($hdr) { # assume JSON, maybe validated + return $validated ? 'openapi' : 'json'; + } + + my @opt_request_list = split ',', $hdr; + my $default_order_val = scalar @opt_request_list; + + my @all_opts; + for my $option (@opt_request_list) { + my $specificity_qual = 0; + my $order_qual = $default_order_val--; + my $user_qual = 1.0; + my $tokens = { map {($_ => $default_tokens{$_})} keys %default_tokens }; + + $option =~ s/^\s+|\s+$//g; + + my ($type, @other) = split ';', $option; + $type =~ s/^\s+|\s+$//g; + + my ($main_type, $sub_type) = split '/', $type; + $specificity_qual++ if ($main_type ne '*'); + $specificity_qual++ if ($sub_type ne '*'); + + for my $maybe_qual (@other) { + $maybe_qual =~ s/^\s+|\s+$//g; + + my ($token_key, $token_value) = split '=', $maybe_qual; + if (defined($token_key) and defined($token_value)) { + $token_key =~ s/^\s+|\s+$//g; + $token_value =~ s/^\s+|\s+$//g; + $$tokens{$token_key} = $token_value; + } + } + + # we only use the q=$weight part, but record the rest, I guess + $user_qual *= $$tokens{q} + if (exists($$tokens{q}) and $$tokens{q} =~ /^\d+(?:\.\d+)?$/); + + push @all_opts, { + full_type => $type, + main_type => $main_type, + sub_type => $sub_type, + tokens => $tokens, + oqual => $order_qual, + squal => $specificity_qual, + uqual => $user_qual + }; + } + + @all_opts = sort { # sort on weight/specificity/order + $$b{uqual} <=> $$a{uqual} || # order they asked for via q-metric, larger is first + $$b{squal} <=> $$a{squal} || # specific rather than wildcard, more specific (larger) is first + $$b{oqual} <=> $$a{oqual} # order specified by the user; counts down, so larger is first + } @all_opts; + + $c->stash(eg_req_accepts => \@all_opts); + + my ($have_xml) = grep { /xml$/ } @resp_types; + my ($have_json) = grep { /json$/ } @resp_types; + my ($have_html) = grep { /^text\/html$/ } @resp_types; + my ($have_text) = grep { /^text\/plain$/ } @resp_types; + my ($have_binary) = grep { /octet/ } @resp_types; + + for my $req_opt (@all_opts) { + my $want_xml = ($$req_opt{sub_type} =~ /xml$/) ? 1 : 0; + my $want_json = ($$req_opt{sub_type} =~ /json$/) ? 1 : 0; + my $want_html = ($$req_opt{full_type} eq 'text/html') ? 1 : 0; + my $want_text = ($$req_opt{main_type} =~ /^text/) ? 1 : 0; + my $want_binary = ($$req_opt{sub_type} =~ /octet/) ? 1 : 0; + + if ($want_xml and $have_xml) { + $c->res->headers->content_type($have_xml); + $c->stash(format => 'xml'); + $c->stash(eg_req_resolved_content_type_object => $req_opt); + $c->stash(eg_req_resolved_content_format => 'xml'); + $c->stash(eg_req_resolved_content_renderer => 'text'); + } elsif ($want_json and $have_json) { + $c->res->headers->content_type($have_json); + $c->stash(eg_req_resolved_content_type_object => $req_opt); + $c->stash(eg_req_resolved_content_format => 'json'); + $c->stash(eg_req_resolved_content_renderer => $validated ? 'openapi' : 'json'); + } elsif ($want_html and $have_html) { + $c->res->headers->content_type($have_html); + $c->stash(format => 'html'); + $c->stash(eg_req_resolved_content_type_object => $req_opt); + $c->stash(eg_req_resolved_content_format => 'html'); + $c->stash(eg_req_resolved_content_renderer => 'html'); + } elsif ($want_text and $have_text) { + $c->res->headers->content_type($have_text); + $c->stash(eg_req_resolved_content_type_object => $req_opt); + $c->stash(eg_req_resolved_content_format => 'text'); + $c->stash(eg_req_resolved_content_renderer => 'text'); + } elsif ($want_binary and $have_binary) { + $c->res->headers->content_type($have_binary); + $c->stash(eg_req_resolved_content_type_object => $req_opt); + $c->stash(eg_req_resolved_content_format => 'binary'); + $c->stash(eg_req_resolved_content_renderer => 'data'); + } + + return $c->stash('eg_req_resolved_content_renderer') + if ($c->stash('eg_req_resolved_content_renderer')); + } + + # push the default tokens from the X-EG-OpenAPI-Options header into the environment + $c->stash(eg_req_resolved_content_type_object => { tokens => \%default_tokens }); + + # found nothing matching above, we'll just use a default + return $validated ? 'openapi' : 'json'; +} + +sub automated_handler_wrapper { + my $endpoint = shift; + my $param_text = $endpoint->method_params || ''; + my $plist = [grep {$_} split /\s+/, $param_text]; + + # opensrf app names contain .s + return opensrf_passthrough_wrapper( $endpoint->method_source, $endpoint->method_name, $plist, $endpoint ) + if ($endpoint->method_source =~ /\./); + + # must be an MJ controller method + my $pkg = $endpoint->method_source; + + my $built_in = $pkg.'::VERSION'; + { no strict 'refs'; + # check to see if we already have the namespace, somehow + unless (eval '$'.$built_in) { + $pkg->use or die $@; + } + } + + my $code = $pkg . '::' . $endpoint->method_name; + return basic_handler_wrapper($plist => $code => $endpoint); +} + +sub is_rendered_success_type { + my $type = shift; + my $ep = shift; + my $renderer = shift; + return 0 unless ( grep {$renderer eq $_} qw/json openapi/ ); + + my ($success_res) = grep {$_->status == 200} @{$ep->responses}; + return 0 unless $success_res; + + return $success_res->schema_type eq $type ? 1 : 0; +} + +sub basic_handler_wrapper { + my $stash_parts = shift; + my $code = shift; + my $endpoint = shift; + + return sub { + my $c = shift->openapi->valid_input or return; + apply_locale($c); + + my $renderer = resolve_requested_render_type($c, $endpoint); + my @params = _build_handler_params($c, $stash_parts); + + my $result; + try { + if (ref($code) and ref($code) eq 'CODE') { # code ref, run it directly + $result = $code->( $c, @params ); + } else { # not a code ref, must be a package-qualified function name, call it + no strict 'refs'; + $result = &{$code}($c, @params ); + } + } catch Error with { + my $e = shift; + $result = { error => $e }; + $c->res->code(500); + }; + + + if ($c->res->is_error) { + $renderer = ref($result) ? 'json' : 'text'; + $result //= ''; + } elsif (!ref($result) and is_rendered_success_type(boolean => $endpoint, $renderer)) { + $result = JSON_bool($result); + } elsif (!ref($result) and is_rendered_success_type(integer => $endpoint, $renderer)) { + $result = 0+$result; + } elsif (!ref($result) and is_rendered_success_type(number => $endpoint, $renderer)) { + $result = 0.0+$result; + } elsif (!ref($result) and is_rendered_success_type(string => $endpoint, $renderer)) { + $result = ''.$result; + } + + return $c->render( $renderer => to_bare_mixed_ref( $result, $c ) ); + }; +} + +sub opensrf_passthrough_wrapper { + my $service = shift; + my $method = shift; + my $stash_parts = shift; + my $endpoint = shift; + + return sub { + my $c = shift->openapi->valid_input or return; + apply_locale($c); + + my $renderer = resolve_requested_render_type($c, $endpoint); + + my $result; + + try { + $result = $U->simplereq( + $service => $method, + _build_handler_params($c, $stash_parts) + ); + + # TODO: Some OpenSRF methods only communicate ILS Events, so + # this may need to be thought through a bit before enabling. + # + # throw $result if ($U->is_event($result) and $result->{textcode} ne 'SUCCESS'); + + } catch Error with { + my $e = shift; + $result = { error => $e }; + $c->res->code(500); + }; + + if ($c->res->is_error) { + $renderer = ref($result) ? 'json' : 'text'; + $result //= ''; + } elsif (!ref($result) and is_rendered_success_type(boolean => $endpoint, $renderer)) { + $result = JSON_bool($result); + } elsif (!ref($result) and is_rendered_success_type(integer => $endpoint, $renderer)) { + $result = 0+$result; + } elsif (!ref($result) and is_rendered_success_type(number => $endpoint, $renderer)) { + $result = 0.0+$result; + } elsif (!ref($result) and is_rendered_success_type(string => $endpoint, $renderer)) { + $result = ''.$result; + } + return $c->render( $renderer => to_bare_mixed_ref( $result, $c ) ); + }; +} + +sub construct_subschema { + my $thing = shift; # fm object from openapi.endpoint_param or openapi.endpoint_response + + my $new_subschema = {}; + my $working = $new_subschema; + + my $stype = $thing->schema_type; + if ($stype and $stype eq 'array') { + $$working{type} = 'array'; + $$working{items} = {}; + $working = $$working{items}; + $stype = $thing->array_items; + } elsif ($thing->can('default_value')) { + $$working{default} = $thing->default_value if $thing->default_value; + } + + if ($thing->fm_type) { + $$working{'$ref'} = '#/components/schemas/'.$thing->fm_type; + } elsif ($stype) { + $$working{type} = $stype; + $$working{format} = $thing->schema_format if ($thing->schema_format); + } else { # no fm_type, no schema_type, allow anything + # See: https://swagger.io/docs/specification/v3_0/data-models/data-types/#any-type and schema def + $$working{'$ref'} = '#/components/schemas/AnyValue'; + } + + return $new_subschema; +} + +sub JSON_bool { + my $val = shift; + return $U->is_true($val) ? Types::Serialiser::true : Types::Serialiser::false; +} + +our %_path_config_cache; +sub fetch_path_config { + my $path = shift; + my $force = shift; + + if (defined($_path_config_cache{$path}) and !$force) { + return $_path_config_cache{$path}; + } + + my $e = new_editor(); + my $path_configs = $e->search_openapi_endpoint([ + {path => $path, active => 't'}, + {flesh => 3, + flesh_fields => + { oep => [qw/perms perm_sets endpoint_sets parameters responses/], # openapi.endpoint + oes => [qw/perms perm_sets/], # openapi.endpoint_set + ops => [qw/perms/] # openapi.perm_set + } + } + ]); + + $_path_config_cache{$path} = {}; # reset it on new or forced reload + for my $op ( @$path_configs ) { + my $p = $_path_config_cache{$path}{$op->http_method} = { + operationId => $op->operation_id, + summary => $op->summary, + tags => [map {$_->name} grep {$_->active eq 't'} @{$op->endpoint_sets}], + }; + + $$p{parameters} = [ + map {{ required => JSON_bool($_->required), name => $_->name, in => $_->in_part, schema => construct_subschema($_) }} @{$op->parameters} + ] if @{$op->parameters}; + + # gather groups of permissions + my @perm_defs; + + push @perm_defs, map {$_->code} @{$op->perms}; # perms assigned to the operationId, each allows access by itself + for my $pset (@{$op->perm_sets}) { + push @perm_defs, join(',', map {$_->code} @{$pset->perms}); # perms for each directly attached perm set, all are required for access + } + for my $eset (@{$op->endpoint_sets}) { + push @perm_defs, map {$_->code} @{$op->perms}; # perms assigned to an endpoint_set (tag) that this operationId belongs to, each allows access by itself + for my $pset (@{$eset->perm_sets}) { + push @perm_defs, join(',', map {$_->code} @{$pset->perms}); # perms for each endpoint_set attached perm set, all are required for access + } + } + + # Wire up the perms we found + $$p{security} = [{$op->security => ['operationId:'.$op->operation_id, @perm_defs]}]; + + $$p{responses} = {}; + for my $res (@{$op->responses}) { + $$p{responses}{$res->status} ||= { + description => $res->description, + content => {} + }; + $$p{responses}{$res->status}{content}{$res->content_type} = { + schema => construct_subschema($res) + }; + } + + # Provide a sane success response schema, even though it won't be validated + $$p{responses}{200} ||= { + description => 'Success', + content => { + 'application/json' => { + schema => { '$ref' => '#/components/schemas/AnyValue' } + } + } + }; + + $_path_config_cache{$path}{$op->http_method} = [$p => automated_handler_wrapper($op)]; + } + + return $_path_config_cache{$path}; +} + +sub add_path { + my $path = shift; + (my $oapath = $path) =~ s/:(\w+)/{$1}/g; + + while (@_) { + my $method = shift; + my $def= shift; + my $handler = shift; + + $method = [$method] unless ref $method; + + for my $m (@$method) { + my $m_def = { %$def }; + + # We only disambiguate when multiple HTTP methods + # are registered with one operation id. The in-db + # config cannot do that, so it's just some builtins + # and early prototyping methods. + $$m_def{operationId} .= "_$m" if @$method > 1; + + unless (exists $$config{paths}{$oapath}{$m}) { + $$config{paths}{$oapath}{$m} = $m_def; + $m = 'del' if ($m eq 'delete'); # for registration via del() + + my $defaults = { + operationId => $$m_def{operationId}, + log_error => 0, + log_allowed => 0 + }; + + &{\&{$m}}( # get, post, put, patch, del, etc, from MJ::L + $path => $defaults => $handler => $$m_def{operationId} + ); + } + } + } +} + +sub _t_f { + my $bool = shift; + return 'f' if ($bool and $bool eq 'f'); + return !!$bool ? 't' : 'f'; +} + +sub log_authentication_attempt { + my $c = shift; + my $user = shift; + my $token = shift; + + my $authen_attempt_log = Fieldmapper::openapi::authen_attempt_log->new; + $authen_attempt_log->request_id( $c->req->request_id ); + $authen_attempt_log->ip_addr( $c->forwarded_for ); + $authen_attempt_log->cred_user( $user ); + $authen_attempt_log->token( $token ); + + my $e = new_editor(xact=>1); + $e->create_openapi_authen_attempt_log($authen_attempt_log); + $e->commit; +} + +sub log_authenticated_dispatch_event { + my $c = shift; + + my $oep_dispatch_log = Fieldmapper::openapi::endpoint_dispatch_log->new; + $oep_dispatch_log->request_id( $c->req->request_id ); + $oep_dispatch_log->error( _t_f($c->stash('log_error')) ); + + my $e = new_editor(xact=>1); + $e->create_openapi_endpoint_dispatch_log($oep_dispatch_log); + $e->commit; +} + +sub log_authenticated_endpoint_access_attempt_event { + my $c = shift; + + my $oep_access_log = Fieldmapper::openapi::endpoint_access_attempt_log->new; + $oep_access_log->request_id( $c->req->request_id ); + $oep_access_log->token( $c->stash('eg_auth_token') ); + $oep_access_log->ip_addr( $c->forwarded_for ); + $oep_access_log->accessor( $c->stash('eg_user_id') ); + $oep_access_log->endpoint( $c->stash('operationId') ); + $oep_access_log->allowed( _t_f($c->stash('log_allowed')) ); + + my $e = new_editor(xact=>1); + $e->create_openapi_endpoint_access_attempt_log($oep_access_log); + $e->commit; +} + +sub securityCheck { + my ($type, $c, $definition, $scopes, $cb) = @_; + $scopes = ref($scopes) ? [@$scopes] : [$scopes]; # make a copy, and make it an arrayref + + if ($$scopes[0] =~ /^operationId:(\S+)/) { # special scope value marker for passing OpId to the under() handler + $c->stash(operationId => $1); + shift @$scopes + } + + return $cb->($c) if ($U->is_true($c->stash('log_allowed'))); # already successfully completed, move on + + my $fail_message = 'Request not allowed'; # generic error message + $c->app->log->trace("Security check [$type] for operationId: ".$c->stash('operationId')); + + my $ses = [split ' ', $c->req->headers->authorization // '']->[-1] + || $c->cookie('eg.api.token') + || $c->cookie('eg.auth.token') + || $c->cookie('ses') + || $c->req->param('ses') + // ''; + + $ses =~ s/\s+//; + $ses =~ s/^%22//; $ses =~ s/%22$//; + $ses =~ s/^['"]//; $ses =~ s/['"]$//; + + if (!$ses) { + $fail_message = 'No authtoken provided'; + } else { + $c->app->log->trace("Received auth token: $ses"); + $c->stash(eg_auth_token => $ses); + + my ($user_obj, $evt) = $U->checkses($ses); + if ($evt) { + log_authenticated_endpoint_access_attempt_event($c); + $fail_message = 'Invalid session'; + } else { + $c->stash(eg_user_obj => $user_obj); + $c->stash(eg_user_id => $user_obj->id); + + $c->app->log->trace("Stash updated for user: ".$c->stash('eg_user_id')); + + my $pass = @$scopes ? 0 : 1; # No perms necessary? You can proceed. + for my $s (@$scopes) { + my $evt = $U->check_perms($user_obj->id, $user_obj->home_ou, split(',',$s)); + $pass++ unless $evt; + } + + if (!$pass) { + $fail_message = 'Permission denied'; + } else { + my $throttle_user = check_request_limits( + $c->stash('operationId'), + $c->stash('eg_user_id'), + $c->forwarded_for + ); + + if ($throttle_user) { + $fail_message = 'Rate limit exceeded'; + $c->res->code(429); + $c->res->headers->header('Retry-After' => $throttle_user); + } else { + $fail_message = undef; # we did it! no failure! + $c->stash(log_allowed => 1); + } + } + } + } + + log_authenticated_endpoint_access_attempt_event($c); + return $cb->($c => $fail_message); +} + +sub check_request_limits { + my $endpoint = shift; + my $user = shift; + my $ip = shift; + + my $limits = new_editor()->json_query({from => ['openapi.check_generic_endpoint_rate_limit', $endpoint, $user, $ip]}); + return $$limits[0]{'openapi.check_generic_endpoint_rate_limit'} if @$limits; + + return undef; # proceed +} + +sub generate_schemas { + my %schemas = (AnyValue => {nullable => Types::Serialiser::true}); + for my $c (Fieldmapper->classes) { + my $h = $c->json_hint; + my $required = $c->Identity; + + $schemas{$h} = { + type => 'object', + properties => {}, + }; + + for my $p ($c->properties) { + my $info = $c->FieldInfo($p); + + my $real_type = $$info{primitive} || $$info{datatype} || ''; + my $type = $$info{datatype} || 'text'; + my $format; + my $nullable = !$$info{required} ? Types::Serialiser::true : Types::Serialiser::false; + + #XXX Fixing some broken data + $type = 'string' if ($real_type eq 'string' and $type eq 'float'); + $nullable = Types::Serialiser::true if ($h eq 'au' and $p eq 'passwd'); + + if ($type eq 'timestamp') { + ($type,$format) = (string => 'date-time'); + } elsif ($type eq 'id') { + ($type,$format) = (string => 'identifier'); + } elsif ($type eq 'text') { + ($type,$format) = (string => undef); + } elsif ($type eq 'money') { + ($type,$format) = (string => 'money'); + } elsif ($type eq 'bool') { + ($type,$format) = (boolean => undef); + } elsif ($type eq 'org_unit') { + ($type,$format) = (link => undef); + } elsif ($type eq 'int') { + ($type,$format) = (integer => 'int64'); + } elsif ($type eq 'number' || $type eq 'float') { + ($type,$format) = (number => 'float'); + } elsif ($type eq 'interval') { + ($type,$format) = (string => 'interval'); + } elsif ($type ne 'link') { + ($type,$format) = (string => undef); + } + + my $ref; + if ($type eq 'link' and my $link = $c->FieldLink($p)) { + $ref = { oneOf => [ { format => $$link{class}, type => 'string', nullable => $nullable }, { '$ref' => "#/components/schemas/$$link{class}" } ] }; + $ref = $$link{reltype} eq 'has_many' ? { nullable => $nullable, type => array => items => $ref } : $ref; + } else { + $type = 'string' if $type eq 'link'; # fallback + $ref = { nullable => $nullable, type => $type }; + #$$ref{format} = $format if ($format); + } + + $schemas{$h}{properties}{$p} = $ref + } + } + return \%schemas; +} + +sub apply_locale { + OpenSRF::AppSession->reset_locale; + OpenSRF::AppSession->default_locale( + parse_eg_locale( + parse_accept_lang($_[0]->req->headers->accept_language) || 'en_us' + ) + ); +} + +sub parse_accept_lang { + my $al = shift; + return undef unless $al; + my ($locale) = split(/,/, $al); + ($locale) = split(/;/, $locale); + return undef unless $locale; + $locale =~ s/-/_/og; + return $locale; +} + +# Accept-Language uses locales like 'en', 'fr', 'fr_fr', while Evergreen +# internally uses 'en-US', 'fr-CA', 'fr-FR' (always with the 2 lowercase, +# hyphen, 2 uppercase convention) +sub parse_eg_locale { + my $ua_locale = shift || 'en_us'; + + $ua_locale =~ m/^(..).?(..)?$/; + my $lang_code = lc($1); + my $region_code = $2 ? uc($2) : uc($1); + return "$lang_code-$region_code"; +} + +sub to_bare_mixed_ref { + my $thing = shift; + my $c = shift; + + my $filter_nulls = ref($c) ? 0 : $c; + + if ($c and ref($c)) { + if (my $type_obj = $c->stash('eg_req_resolved_content_type_object')) { + $filter_nulls = $U->is_true($$type_obj{tokens}{filterNulls}) ? 1 : 0; + } + + unless ($c->res->is_error) { # don't trim error output + my $op = $c->stash('operationId'); + my $user_obj = $c->stash('eg_user_obj'); + + if ($op and $user_obj) { + + # First, gather user settings applied to integrators + my $usettings = new_editor()->search_actor_user_setting({ + usr => $user_obj->id, + name => ["REST.api.whitelist_properties.$op","REST.api.whitelist_properties", + "REST.api.blacklist_properties.$op","REST.api.blacklist_properties"] + }); + my @whitelist_settings = map { OpenSRF::Utils::JSON->JSON2perl($_->value) } grep { $_->name =~ /^REST.api.whitelist_properties/ } @$usettings; + my @blacklist_settings = map { OpenSRF::Utils::JSON->JSON2perl($_->value) } grep { $_->name =~ /^REST.api.blacklist_properties/ } @$usettings; + + # Then add YAOUSen to the list + my %YAOUSen = $U->ou_ancestor_setting_batch_insecure( + $user_obj->home_ou, + ["REST.api.whitelist_properties.$op","REST.api.whitelist_properties", + "REST.api.blacklist_properties.$op","REST.api.blacklist_properties"] + ); + push @whitelist_settings, grep {defined and /\w+/} map { defined($YAOUSen{$_}) ? $YAOUSen{$_}{value} : undef } grep {/^REST.api.whitelist_properties/} keys %YAOUSen; + push @blacklist_settings, grep {defined and /\w+/} map { defined($YAOUSen{$_}) ? $YAOUSen{$_}{value} : undef } grep {/^REST.api.blacklist_properties/} keys %YAOUSen; + + do { + $filter_nulls++; # force filtering of nulls because some properties can't be null but are allowed to not exist at all + $thing = filter_object_properties($c, $thing, join(',', @whitelist_settings), join(',', @blacklist_settings)); + } if (@whitelist_settings or @blacklist_settings); + } + } + } + + $thing = $thing->to_bare_hash if (blessed($thing) and $thing->isa('Fieldmapper')); + + my $thing_type = ref($thing); + return $thing unless $thing_type; + + if ($thing_type eq 'HASH') { + return filter_out_null_properties( { map { ($_, to_bare_mixed_ref($$thing{$_}, $c)) } keys %$thing }, $filter_nulls ); + } elsif ($thing_type eq 'ARRAY') { + return [ map { to_bare_mixed_ref($_, $c) } @$thing ]; + } + + # dunno what to do with it... + return $thing; +} + +sub filter_out_null_properties { + my $hashref = shift; + my $do_filter = shift; + return $hashref unless $do_filter; + + my %replacement; + for my $key (keys %$hashref) { + $replacement{$key} = $$hashref{$key} if defined $$hashref{$key}; + } + + return \%replacement; +} + +sub filter_object_properties { + my $c = shift; + my $obj = shift; + my $keep_proplist = shift || ''; + my $toss_proplist = shift || ''; + my $shallow = shift; + + return $obj unless ($keep_proplist or $toss_proplist); + + if (!ref($keep_proplist)) { # passed a comma separated list instead of an arrayref + $keep_proplist = [ grep {/\w+\.\w+/} map {s/^\s+//; s/\s+$//; $_} split ',', $keep_proplist ]; + } + + if (!ref($toss_proplist)) { # passed a comma separated list instead of an arrayref + $toss_proplist = [ grep {/\w+\.\w+/} map {s/^\s+//; s/\s+$//; $_} split ',', $toss_proplist ]; + } + + return $obj unless (@$keep_proplist or @$toss_proplist); + + if (blessed($obj) and $obj->isa('Fieldmapper')) { # working an an FM object + my $hint = $obj->json_hint; + $c->app->log->trace("Checking for field restrictions on object type $hint"); + my @my_keep_proplist = grep { /^$hint\./ } @$keep_proplist; + my @my_toss_proplist = grep { /^$hint\./ } @$toss_proplist; + if (@my_keep_proplist or @my_toss_proplist) { # we must have /at least/ one property to turn on filtering + for my $prop ($obj->properties) { + #$c->app->log->trace("Checking for field restrictions on object type $hint, property $prop"); + if (defined $obj->$prop && + (@my_keep_proplist and !grep { $_ eq "$hint.$prop" } @my_keep_proplist) || # we have a "keep" list but the property is not on it + (@my_toss_proplist and grep { $_ eq "$hint.$prop" } @my_toss_proplist) # or, we have a "toss" list and the property IS on it + ) { + $c->app->log->trace("Clearing property $prop on object type $hint"); + my $clear_func = "clear_$prop"; + $obj->$clear_func; + } elsif (!$shallow) { + $obj->$prop(filter_object_properties($c, $obj->$prop, $keep_proplist, $toss_proplist)); + } + } + } + } elsif (ref($obj) and ref($obj) eq 'ARRAY') { # an arrayref of stuff + $obj = [ map { filter_object_properties($c, $_, $keep_proplist, $toss_proplist, $shallow) } @$obj ]; + } elsif (ref($obj) and ref($obj) eq 'HASH') { # a hashref of stuff + $obj = { map { ($_ => filter_object_properties($c, $$obj{$_}, $keep_proplist, $toss_proplist, $shallow)) } keys %$obj }; + } + + return $obj; +} + +# This package adds format validators for all IDL class hints +package JSON::Validator::Schema::OpenAPIv3::Evergreen; +use JSON::Validator::Schema::OpenAPIv3; +use OpenSRF::Utils::Logger q/$logger/; +use OpenILS::Utils::Fieldmapper; +use base 'JSON::Validator::Schema::OpenAPIv3'; + +our $_format_cache; +sub _build_formats { + return $_format_cache if $_format_cache; + $_format_cache = JSON::Validator::Schema::OpenAPIv3::_build_formats(); + $logger->info('Adding fieldmapper class hints to OpenAPI format validator mappings'); + $$_format_cache{$_->json_hint} = sub { return undef } for (Fieldmapper->classes); + return $_format_cache; +} + +1; commit c8e65252507ff94dff5e1e19835c356f92321c2b Author: Mike Rylander <mrylander@gmail.com> Date: Tue Dec 10 12:55:26 2024 -0500 LP#2067414: Add OpenAPI Perl dependencies Adds Perl modules required to run the Mojolicious-based OpenAPI server for Evergreen. Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/src/extras/install/Makefile.debian-bookworm b/Open-ILS/src/extras/install/Makefile.debian-bookworm index cf41bfcd5a..a7ebae9809 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-bookworm +++ b/Open-ILS/src/extras/install/Makefile.debian-bookworm @@ -118,6 +118,11 @@ export CPAN_MODULES = \ Text::Levenshtein::Damerau::XS \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Email::Send \ Duadua diff --git a/Open-ILS/src/extras/install/Makefile.debian-bullseye b/Open-ILS/src/extras/install/Makefile.debian-bullseye index fbf4935b61..57b8dfacaf 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-bullseye +++ b/Open-ILS/src/extras/install/Makefile.debian-bullseye @@ -118,6 +118,11 @@ export CPAN_MODULES = \ Email::Send \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Locale::Country \ Duadua diff --git a/Open-ILS/src/extras/install/Makefile.debian-buster b/Open-ILS/src/extras/install/Makefile.debian-buster index c1ef3add99..1a268853f2 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-buster +++ b/Open-ILS/src/extras/install/Makefile.debian-buster @@ -118,6 +118,11 @@ export CPAN_MODULES = \ Text::Levenshtein::Damerau::XS \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Email::Send \ Duadua diff --git a/Open-ILS/src/extras/install/Makefile.fedora b/Open-ILS/src/extras/install/Makefile.fedora index 64543ee2c7..2cff7f378a 100644 --- a/Open-ILS/src/extras/install/Makefile.fedora +++ b/Open-ILS/src/extras/install/Makefile.fedora @@ -92,6 +92,11 @@ export CPAN_MODULES = \ Config::General \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Rose::URI \ Duadua diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-jammy b/Open-ILS/src/extras/install/Makefile.ubuntu-jammy index 90995526dd..635c12e4f1 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-jammy +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-jammy @@ -117,6 +117,11 @@ export CPAN_MODULES = \ String::KeyboardDistance \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Text::Levenshtein::Damerau::XS \ Duadua diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-noble b/Open-ILS/src/extras/install/Makefile.ubuntu-noble index 59f2c955ad..8aecc6baf1 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-noble +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-noble @@ -118,6 +118,11 @@ export CPAN_MODULES = \ String::KeyboardDistance \ Pass::OTP \ Authen::WebAuthn \ + IO::Prompter \ + Mojolicious \ + Mojolicious::Lite \ + Mojolicious::Plugin::OpenAPI \ + Mojolicious::Plugin::ForwardedFor \ Text::Levenshtein::Damerau::XS \ Duadua commit 2a308edb8312a2b5ea422892de5fd7e8e0a2771a Author: Mike Rylander <mrylander@gmail.com> Date: Tue Dec 10 12:52:03 2024 -0500 LP#2067414: API login support in open-ils.auth This commit adds a new login type, api, with associated permission checks and session timeout settings. Both the newer all-in-one calling style and the older CHAP-style code paths are supported. Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 74b5532513..60a42fe73c 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -510,6 +510,7 @@ vim:et:ts=4:sw=4: <!-- defined app-specific settings here --> <default_timeout> <!-- default login timeouts based on login type --> + <api>3600</api> <opac>420</opac> <staff>7200</staff> <temp>300</temp> diff --git a/Open-ILS/include/openils/oils_constants.h b/Open-ILS/include/openils/oils_constants.h index b2ec580ecd..3cc1357984 100644 --- a/Open-ILS/include/openils/oils_constants.h +++ b/Open-ILS/include/openils/oils_constants.h @@ -6,6 +6,7 @@ extern "C" { #endif /* Settings ------------------------------------------------------ */ +#define OILS_ORG_SETTING_API_TIMEOUT "auth.api_timeout" #define OILS_ORG_SETTING_OPAC_TIMEOUT "auth.opac_timeout" #define OILS_ORG_SETTING_STAFF_TIMEOUT "auth.staff_timeout" #define OILS_ORG_SETTING_TEMP_TIMEOUT "auth.temp_timeout" diff --git a/Open-ILS/src/c-apps/oils_auth.c b/Open-ILS/src/c-apps/oils_auth.c index 8234c5a770..38f9fbccf0 100644 --- a/Open-ILS/src/c-apps/oils_auth.c +++ b/Open-ILS/src/c-apps/oils_auth.c @@ -15,9 +15,13 @@ #define OILS_AUTH_OPAC "opac" #define OILS_AUTH_STAFF "staff" +#define OILS_AUTH_API "api" #define OILS_AUTH_TEMP "temp" #define OILS_AUTH_PERSIST "persist" +#define OILS_PASSTYPE_MAIN "main" +#define OILS_PASSTYPE_API "api" + // Default time for extending a persistent session: ten minutes #define DEFAULT_RESET_INTERVAL 10 * 60 @@ -178,11 +182,12 @@ int osrfAppChildInit() { } // free() response -static char* oilsAuthGetSalt(int user_id) { +static char* oilsAuthGetSalt(int user_id, char* ptype) { char* salt_str = NULL; + if (!ptype) ptype = OILS_PASSTYPE_MAIN; jsonObject* params = jsonParseFmt( // free - "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, "main"); + "{\"from\":[\"actor.get_salt\",%d,\"%s\"]}", user_id, ptype); jsonObject* salt_obj = // free oilsUtilsCStoreReq("open-ils.cstore.json_query", params); @@ -209,7 +214,7 @@ static char* oilsAuthGetSalt(int user_id) { // ident is either a username or barcode // Returns the init seed -> requires free(); static char* oilsAuthBuildInitCache( - int user_id, const char* ident, const char* ident_type, const char* nonce) { + int user_id, const char* ident, const char* ident_type, const char* nonce, char* ptype) { char* cache_key = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, ident, nonce); @@ -222,7 +227,7 @@ static char* oilsAuthBuildInitCache( // user does not exist. Use a dummy seed auth_seed = strdup("x"); } else { - auth_seed = oilsAuthGetSalt(user_id); + auth_seed = oilsAuthGetSalt(user_id, ptype); } jsonObject* seed_object = jsonParseFmt( @@ -254,7 +259,7 @@ static char* oilsAuthBuildInitCache( } static int oilsAuthInitUsernameHandler( - osrfMethodContext* ctx, const char* username, const char* nonce) { + osrfMethodContext* ctx, const char* username, const char* nonce, char* ptype) { osrfLogInfo(OSRF_LOG_MARK, "User logging in with username %s", username); @@ -268,7 +273,7 @@ static int oilsAuthInitUsernameHandler( jsonObjectFree(user_obj); // NULL OK - char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce); + char* seed = oilsAuthBuildInitCache(user_id, username, "username", nonce, ptype); resp = jsonNewObject(seed); free(seed); @@ -285,18 +290,23 @@ int oilsAuthInitUsername(osrfMethodContext* ctx) { jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0)); const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1)); + const char* login_type = + jsonObjectGetString(jsonObjectGetIndex(ctx->params, 2)); + + char* ptype = OILS_PASSTYPE_MAIN; + if (login_type && !strcmp(login_type, OILS_AUTH_API)) ptype = OILS_PASSTYPE_API; if (!nonce) nonce = ""; if (!username) return -1; - int resp = oilsAuthInitUsernameHandler(ctx, username, nonce); + int resp = oilsAuthInitUsernameHandler(ctx, username, nonce, ptype); free(username); return resp; } static int oilsAuthInitBarcodeHandler( - osrfMethodContext* ctx, const char* barcode, const char* nonce) { + osrfMethodContext* ctx, const char* barcode, const char* nonce, char* ptype) { osrfLogInfo(OSRF_LOG_MARK, "User logging in with barcode %s", barcode); @@ -310,7 +320,7 @@ static int oilsAuthInitBarcodeHandler( jsonObjectFree(user_obj); // NULL OK - char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce); + char* seed = oilsAuthBuildInitCache(user_id, barcode, "barcode", nonce, ptype); resp = jsonNewObject(seed); free(seed); @@ -328,11 +338,16 @@ int oilsAuthInitBarcode(osrfMethodContext* ctx) { jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0)); const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1)); + const char* login_type = + jsonObjectGetString(jsonObjectGetIndex(ctx->params, 2)); + + char* ptype = OILS_PASSTYPE_MAIN; + if (login_type && !strcmp(login_type, OILS_AUTH_API)) ptype = OILS_PASSTYPE_API; if (!nonce) nonce = ""; if (!barcode) return -1; - int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce); + int resp = oilsAuthInitBarcodeHandler(ctx, barcode, nonce, ptype); free(barcode); return resp; @@ -403,6 +418,7 @@ static int oilsAuthIdentIsBarcode(const char* identifier, int org_id) { is added to the auth init cache to differentiate between logins using the same username and thus avoiding cache collisions for near-simultaneous logins. + - login_type : optional, used to select password type Return to client: Intermediate authentication seed. */ @@ -414,14 +430,19 @@ int oilsAuthInit(osrfMethodContext* ctx) { jsonObjectToSimpleString(jsonObjectGetIndex(ctx->params, 0)); const char* nonce = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1)); + const char* login_type = + jsonObjectGetString(jsonObjectGetIndex(ctx->params, 2)); + + char* ptype = OILS_PASSTYPE_MAIN; + if (login_type && !strcmp(login_type, OILS_AUTH_API)) ptype = OILS_PASSTYPE_API; if (!nonce) nonce = ""; if (!identifier) return -1; // we need an identifier if (oilsAuthIdentIsBarcode(identifier, 0)) { - resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce); + resp = oilsAuthInitBarcodeHandler(ctx, identifier, nonce, ptype); } else { - resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce); + resp = oilsAuthInitUsernameHandler(ctx, identifier, nonce, ptype); } free(identifier); @@ -454,7 +475,7 @@ int oilsAuthInit(osrfMethodContext* ctx) { method or to receive the seed from the process that did so. */ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, - const char* identifier, const char* password, const char* nonce) { + const char* identifier, const char* password, const char* nonce, const char* login_type) { int verified = 0; @@ -464,11 +485,14 @@ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, free(key); // Ask the DB to verify the user's password. - // Here, the password is md5(md5(password) + salt) + // Here, the password is md5(salt + md5(password)) + + char* ptype = OILS_PASSTYPE_MAIN; + if (login_type && !strcmp(login_type, OILS_AUTH_API)) ptype = OILS_PASSTYPE_API; jsonObject* params = jsonParseFmt( // free - "{\"from\":[\"actor.verify_passwd\",%d,\"main\",\"%s\"]}", - user_id, password); + "{\"from\":[\"actor.verify_passwd\",%d,\"%s\",\"%s\"]}", + user_id, ptype, password); jsonObject* verify_obj = // free oilsUtilsCStoreReq("open-ils.cstore.json_query", params); @@ -512,10 +536,11 @@ static int oilsAuthVerifyPassword( const osrfMethodContext* ctx, int user_id, * Turn the password into the nested md5 hash required of migrated * passwords, then check the password in the DB. */ -static int oilsAuthLoginCheckPassword(int user_id, const char* password) { +static int oilsAuthLoginCheckPassword(int user_id, const char* password, char* ptype) { + if (!ptype) ptype = OILS_PASSTYPE_MAIN; growing_buffer* gb = osrf_buffer_init(33); // free me 1 - char* salt = oilsAuthGetSalt(user_id); // free me 2 + char* salt = oilsAuthGetSalt(user_id, ptype); // free me 2 char* passhash = md5sum(password); // free me 3 osrf_buffer_add(gb, salt); // gb strdup's internally @@ -533,7 +558,7 @@ static int oilsAuthLoginCheckPassword(int user_id, const char* password) { jsonObject *arr = jsonNewObjectType(JSON_ARRAY); jsonObjectPush(arr, jsonNewObject("actor.verify_passwd")); jsonObjectPush(arr, jsonNewNumberObject((long) user_id)); - jsonObjectPush(arr, jsonNewObject("main")); + jsonObjectPush(arr, jsonNewObject(ptype)); jsonObjectPush(arr, jsonNewObject(finalpass)); jsonObject *params = jsonNewObjectType(JSON_HASH); // free me 6 jsonObjectSetKey(params, "from", arr); @@ -559,7 +584,9 @@ static int oilsAuthLoginCheckPassword(int user_id, const char* password) { } static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx, - int user_id, const char* username, const char* password) { + int user_id, const char* username, const char* password, const char* login_type) { + char* ptype = OILS_PASSTYPE_MAIN; + if (login_type && !strcmp(login_type, OILS_AUTH_API)) ptype = OILS_PASSTYPE_API; // build the cache key growing_buffer* gb = osrf_buffer_init(64); // free me @@ -587,7 +614,7 @@ static int oilsAuthLoginVerifyPassword(const osrfMethodContext* ctx, } } - int verified = oilsAuthLoginCheckPassword(user_id, password); + int verified = oilsAuthLoginCheckPassword(user_id, password, ptype); if (!verified) { // login failed. increment failure counter. failcount++; @@ -897,7 +924,7 @@ int oilsAuthComplete( osrfMethodContext* ctx ) { // User exists and is not barred, etc. Test the password. passOK = oilsAuthVerifyPassword( - ctx, user_id, identifier, password, nonce); + ctx, user_id, identifier, password, nonce, type); if (!passOK) { // Password check failed. Return generic login failure. @@ -1068,7 +1095,7 @@ int oilsAuthLogin(osrfMethodContext* ctx) { } if (!response && // user exists and is not barred, etc. - !oilsAuthLoginVerifyPassword(ctx, user_id, username, password)) { + !oilsAuthLoginVerifyPassword(ctx, user_id, username, password, type)) { // User provided the wrong password or is blocked from too // many previous login failures. diff --git a/Open-ILS/src/c-apps/oils_auth_internal.c b/Open-ILS/src/c-apps/oils_auth_internal.c index 15dd24aebd..373c8ffba2 100644 --- a/Open-ILS/src/c-apps/oils_auth_internal.c +++ b/Open-ILS/src/c-apps/oils_auth_internal.c @@ -21,6 +21,7 @@ #define OILS_AUTH_STAFF "staff" #define OILS_AUTH_TEMP "temp" #define OILS_AUTH_PERSIST "persist" +#define OILS_AUTH_API "api" #define BLOCK_EXPIRED_STAFF_LOGIN_FLAG "auth.block_expired_staff_login" @@ -37,6 +38,7 @@ static long _oilsAuthOPACTimeout = 0; static long _oilsAuthStaffTimeout = 0; static long _oilsAuthOverrideTimeout = 0; static long _oilsAuthPersistTimeout = 0; +static long _oilsAuthAPITimeout = 0; /** @brief Initialize the application by registering functions for method calls. @@ -145,6 +147,15 @@ static long oilsAuthGetTimeout( _oilsAuthOPACTimeout = 0; } + value_obj = osrf_settings_host_value_object( + "/apps/open-ils.auth_internal/app_settings/default_timeout/api" ); + _oilsAuthAPITimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj )); + jsonObjectFree(value_obj); + if( -1 == _oilsAuthAPITimeout ) { + osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for API logins" ); + _oilsAuthAPITimeout = 0; + } + value_obj = osrf_settings_host_value_object( "/apps/open-ils.auth_internal/app_settings/default_timeout/staff" ); _oilsAuthStaffTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj )); @@ -173,9 +184,9 @@ static long oilsAuthGetTimeout( } osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeouts: " - "opac => %ld : staff => %ld : temp => %ld : persist => %ld", + "opac => %ld : staff => %ld : temp => %ld : persist => %ld : api => %ld", _oilsAuthOPACTimeout, _oilsAuthStaffTimeout, - _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout ); + _oilsAuthOverrideTimeout, _oilsAuthPersistTimeout, _oilsAuthAPITimeout ); } int home_ou = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" )); @@ -188,6 +199,9 @@ static long oilsAuthGetTimeout( if( !strcmp( type, OILS_AUTH_OPAC )) { setting = OILS_ORG_SETTING_OPAC_TIMEOUT; default_timeout = _oilsAuthOPACTimeout; + } else if( !strcmp( type, OILS_AUTH_API )) { + setting = OILS_ORG_SETTING_API_TIMEOUT; + default_timeout = _oilsAuthAPITimeout; } else if( !strcmp( type, OILS_AUTH_STAFF )) { setting = OILS_ORG_SETTING_STAFF_TIMEOUT; default_timeout = _oilsAuthStaffTimeout; @@ -275,6 +289,9 @@ static oilsEvent* oilsAuthCheckLoginPerm(osrfMethodContext* ctx, if (!strcasecmp(type, OILS_AUTH_OPAC)) { perms[0] = "OPAC_LOGIN"; + } else if (!strcasecmp(type, OILS_AUTH_API)) { + perms[0] = "API_LOGIN"; + } else if (!strcasecmp(type, OILS_AUTH_STAFF)) { perms[0] = "STAFF_LOGIN"; commit 5c79bf7d69d99f0e72882d46b112d51d3162d539 Author: Mike Rylander <mrylander@gmail.com> Date: Tue Dec 10 12:55:03 2024 -0500 LP#2067414: Fieldmapper utility class enhancements This commit adds enhancements to the Fieldmapper utility code for deserializing and reserializating nested IDL-based objects. Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm index 7ff10d293f..2dea5a0edf 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm @@ -6,6 +6,7 @@ use OpenSRF::Utils::SettingsClient; use OpenSRF::System; use XML::LibXML; use Scalar::Util 'blessed'; +use Types::Serialiser; my $log = 'OpenSRF::Utils::Logger'; @@ -423,6 +424,13 @@ sub FieldDatatype { return $$fieldmap{$class_name}{fields}{$field}{datatype}; } +sub FieldLink { + my $self = shift; + my $f = shift; + return undef unless ($f && exists $$fieldmap{$self->class_name}{links}{$f}); + return $$fieldmap{$self->class_name}{links}{$f}; +} + sub class_name { my $class_name = shift; return ref($class_name) || $class_name; @@ -457,10 +465,38 @@ sub properties { sub to_bare_hash { my $self = shift; + my $deep = shift; + my $cname = $self->class_name; my %hash = (); for my $f ($self->properties) { my $val = $self->$f; + my $vtype = $cname->FieldDatatype($f) || ''; + if ($deep + and ref($val) + and exists $$fieldmap{$cname}{links}{$f} + ) { + my $fclass = Fieldmapper::class_for_hint($$fieldmap{$cname}{links}{$f}{class}); + if ($fclass and $$fieldmap{$cname}{links}{$f}{reltype} eq 'has_many' and @$val) { + $val = [ map { (blessed($_) and $_->isa('Fieldmapper')) ? $_->to_bare_hash($deep) : $_ } @$val ]; + } elsif (blessed($val) and $val->isa('Fieldmapper')) { + $val = $val->to_bare_hash($deep); + } + } elsif (defined($val) and $vtype eq 'bool') { + $val = ($val and $val !~ /^f$/i) ? Types::Serialiser::true : Types::Serialiser::false; + } elsif ( + $val + and $vtype eq 'timestamp' + and $val =~ /^(\S{10}T\S{8}[-+]\d{2})(\d{2})$/ + ) { + $val = "$1:$2"; + } elsif ( + $val + and $vtype eq 'timestamp' + and $val =~ /^\S{10}$/ + ) { + $val .= 'T00:00:00'; + } $hash{$f} = $val; } @@ -473,9 +509,24 @@ sub to_bare_hash { sub from_bare_hash { my $self = shift; my $hash = shift; + my $deep = shift; + my $cname = $self->class_name; + my @value = (); for my $f ($self->properties) { - push @value, $$hash{$f}; + my $val = $$hash{$f}; + if ($deep + and ref($val) + and $self->FieldDatatype($f) eq 'link' + ) { + my $fclass = Fieldmapper::class_for_hint($$fieldmap{$cname}{links}{$f}{class}); + if ($fclass and $$fieldmap{$cname}{links}{$f}{reltype} eq 'has_many' and @$val) { + $val = [ map { $fclass->from_bare_hash($_, $deep) } @$val ]; + } else { + $val = $fclass->from_bare_hash($val, $deep); + } + } + push @value, $val; } return $self->new(\@value); } commit 461e5cda890ebaab4d013f8e0b3a14e50e09b6e7 Author: Mike Rylander <mrylander@gmail.com> Date: Fri Dec 20 16:17:39 2024 -0500 LP#2067414: Existing API enhancements In order to efficiently and flexibly generate the data likely needed by RESTful OpenAPI clients, a couple small enhancements to existing OpenSRF methods are provided here: * Enhance display field output retrieval * Add sorting option to MBTS retrieval Signed-off-by: Mike Rylander <mrylander@gmail.com> Signed-off-by: Ruth Frasur Davis <rdavis@evergreencdi.org> Signed-off-by: Galen Charlton <gmc@equinoxOLI.org> Signed-off-by: Jeff Godin <jgodin@tadl.org> diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm index 6d724695dc..ec2722990a 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm @@ -2574,7 +2574,7 @@ sub user_transaction_history { $filter->{'total_owed'} = { '<>' => 0 }; } - my $options_clause = { order_by => { mbt => 'xact_start DESC' } }; + my $options_clause = { order_by => { mbt => 'xact_start '.($$options{sort} ? uc($$options{sort}) : 'DESC') } }; $options_clause->{'limit'} = $options->{'limit'} if $options->{'limit'}; $options_clause->{'offset'} = $options->{'offset'} if $options->{'offset'}; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm index 9e96b0ad6b..16f42e48f5 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm @@ -1648,14 +1648,21 @@ sub fetch_display_fields { } my $e = new_editor(); + my $fleshed = 0; + my %df_cache; + + if ($self->api_name =~ /fleshed$/) { + $fleshed++; + %df_cache = map { + ($_->id => {%{$_->to_bare_hash}{qw/id field_class name label search_field browse_field facet_field display_field restrict/}}) + } @{ $e->retrieve_all_config_metabib_field }; + } for my $record ( @records ) { next unless ($record && $highlight_map); - $conn->respond( - $e->json_query( - {from => ['search.highlight_display_fields', $record, $highlight_map]} - ) - ); + my $hl = $e->json_query({from => ['search.highlight_display_fields', $record, $highlight_map]}); + $hl = [ map { $$_{field} = $df_cache{$$_{field}}; $_ } @$hl ] if $fleshed; + $conn->respond( $hl ); } return undef; @@ -1666,6 +1673,12 @@ __PACKAGE__->register_method( stream => 1 ); +__PACKAGE__->register_method( + method => 'fetch_display_fields', + api_name => 'open-ils.search.fetch.metabib.display_field.highlight.fleshed', + stream => 1 +); + sub tag_circulated_records { my ($auth, $results, $metabib) = @_; ----------------------------------------------------------------------- Summary of changes: Open-ILS/examples/fm_IDL.xml | 468 ++++- Open-ILS/examples/opensrf.xml.example | 1 + Open-ILS/include/openils/oils_constants.h | 1 + Open-ILS/src/c-apps/oils_auth.c | 73 +- Open-ILS/src/c-apps/oils_auth_internal.c | 21 +- .../src/extras/install/Makefile.debian-bookworm | 5 + .../src/extras/install/Makefile.debian-bullseye | 5 + Open-ILS/src/extras/install/Makefile.debian-buster | 5 + Open-ILS/src/extras/install/Makefile.fedora | 5 + Open-ILS/src/extras/install/Makefile.ubuntu-jammy | 5 + Open-ILS/src/extras/install/Makefile.ubuntu-noble | 5 + .../src/perlmods/lib/OpenILS/Application/Actor.pm | 2 +- .../lib/OpenILS/Application/Search/Biblio.pm | 23 +- .../src/perlmods/lib/OpenILS/OpenAPI/Controller.pm | 149 ++ .../perlmods/lib/OpenILS/OpenAPI/Controller/bib.pm | 236 +++ .../lib/OpenILS/OpenAPI/Controller/course.pm | 60 + .../lib/OpenILS/OpenAPI/Controller/hold.pm | 117 ++ .../perlmods/lib/OpenILS/OpenAPI/Controller/org.pm | 44 + .../lib/OpenILS/OpenAPI/Controller/patron.pm | 316 +++ .../src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm | 53 +- Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/005.schema.actors.sql | 39 - Open-ILS/src/sql/Pg/610.schema.openapi.sql | 446 +++++ Open-ILS/src/sql/Pg/950.data.seed-values.sql | 955 ++++++++- Open-ILS/src/sql/Pg/999.functions.global.sql | 52 + Open-ILS/src/sql/Pg/sql_file_manifest | 1 + .../src/sql/Pg/upgrade/1468.schema.openapi.sql | 1472 ++++++++++++++ Open-ILS/src/support-scripts/api_ctl | 2043 ++++++++++++++++++++ Open-ILS/src/support-scripts/openapi_server | 1016 ++++++++++ .../assets/images/restful_api/api_explorer.png | Bin 0 -> 41415 bytes .../assets/images/restful_api/block_expanded.png | Bin 0 -> 47107 bytes .../assets/images/restful_api/create_endpoint.png | Bin 0 -> 88878 bytes .../images/restful_api/expected_parameters.png | Bin 0 -> 31761 bytes .../assets/images/restful_api/method_details.png | Bin 0 -> 107160 bytes docs/modules/integrations/nav.adoc | 1 + docs/modules/integrations/pages/restful_api.adoc | 883 +++++++++ 36 files changed, 8427 insertions(+), 77 deletions(-) create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/bib.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/course.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/hold.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/org.pm create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/OpenAPI/Controller/patron.pm create mode 100755 Open-ILS/src/sql/Pg/610.schema.openapi.sql create mode 100755 Open-ILS/src/sql/Pg/upgrade/1468.schema.openapi.sql create mode 100755 Open-ILS/src/support-scripts/api_ctl create mode 100755 Open-ILS/src/support-scripts/openapi_server create mode 100644 docs/modules/integrations/assets/images/restful_api/api_explorer.png create mode 100644 docs/modules/integrations/assets/images/restful_api/block_expanded.png create mode 100644 docs/modules/integrations/assets/images/restful_api/create_endpoint.png create mode 100644 docs/modules/integrations/assets/images/restful_api/expected_parameters.png create mode 100644 docs/modules/integrations/assets/images/restful_api/method_details.png create mode 100644 docs/modules/integrations/pages/restful_api.adoc hooks/post-receive -- Evergreen ILS