From a7764138f5b760e5996c25c21cf6318521009a63 Mon Sep 17 00:00:00 2001 From: Rich Gwozdz Date: Tue, 28 Nov 2023 06:52:27 -0800 Subject: [PATCH] chore: update auth docs (#130) * chore: update auth docs * chore: auth methods for providers --- _docs/basics/faqs.md | 2 +- _docs/basics/provider-types.md | 21 +++++++++++++++++---- _docs/development/authorization.md | 16 +++++++--------- _docs/development/provider/model.md | 16 ++++++++++++++++ _docs/usage/authorization.md | 19 +++++++++++-------- 5 files changed, 52 insertions(+), 22 deletions(-) diff --git a/_docs/basics/faqs.md b/_docs/basics/faqs.md index 05e8639..262c3d7 100644 --- a/_docs/basics/faqs.md +++ b/_docs/basics/faqs.md @@ -49,7 +49,7 @@ You may see Koop related warnings in the console output. Most often these are r > KOOP_WARNINGS=suppress node server.js ``` -### How should I write my provider to hanlde `timestamp`-style date queries? +### How should I write my provider code so that `timestamp` data is properly queried? Some clients that use Feature Services may format date queries in the `timestamp` style, for example `where=my_date >= timestamp '2021-01-01 06:00:00'` ([more information on querying dates](https://www.esri.com/arcgis-blog/products/api-rest/data-management/querying-feature-services-date-time-queries/)). To ensure Koop filters the data properly, you should use the `metadata.fields` property to designate that field (in this case, `my_date`) as `type: "Date"` and also return the data from your provider using the JavaScript [Date.toISOString() function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString). For example, `properties.my_date = (new Date(year, month, date)).toISOString();`. diff --git a/_docs/basics/provider-types.md b/_docs/basics/provider-types.md index 0c013a0..ae5b15e 100644 --- a/_docs/basics/provider-types.md +++ b/_docs/basics/provider-types.md @@ -24,16 +24,29 @@ It makes sense to use a *full-fetch* pattern if at least one of the following is ### Pass-Through -Pass-through providers act as a proxy/translator between the client and the remote API. More specifically, they convert GeoServices API query parameters to the analogous query parameters on the remote API. As a result, pass-through providers only fetch a targeted subset of the remote dataset. As an example, consider this request to the [Google Analytics provider](https://github.com/koopjs/koop-provider-google-analytics): +Pass-through providers act as a proxy/translator between the client and the remote API. More specifically, they convert GeoServices API query parameters to the analogous query parameters on the remote API. As a result, pass-through providers only fetch a targeted subset of the remote dataset. As an example, consider this request to the [Mongo DB provider](https://github.com/koopjs/koop-provider-mongo): ``` -http://localhost:8080/metrics/sessions::views/eventCategory/FeatureServer/0/query?where=country='United States' +http://localhost:8080/mongo/rest/services/fires/FeatureServer/0/query?where=country='United States' ``` -The Google Analytics provider handles this request by converting the GeoServices API `where` parameter to the Google Analytics API equivalent. It then fetches *only* the subset of data with `country` equal to 'United States'. This partial dataset is then passed on to Koop and then back to the client. +The MongoDB provider handles this request by converting the GeoServices API `where` parameter to the MongoDB equivalent. It then fetches *only* the subset of data with `country` equal to 'United States'. This partial dataset is then passed on to Koop and then back to the client. -It's important to note that the pass-through pattern can be partially applied, i.e., you don't have to provide a translation for every GeoService API query parameter. For example, you might author a translation for the `where` parameter, but not the `time` or `geometry` parameters. Partial support for query parameters is still useful, because it reduces the size of the dataset the Koop/Winnow has to loop through to apply other filters or sorts. +It's important to note that the pass-through pattern can be partially applied, i.e., you don't have to provide a translation for every GeoService API query parameter. For example, you might author a translation for the `where` parameter, but not the `time` or `geometry` parameters. Partial support for query parameters is still useful, because it reduces the size of the dataset fetched from the remote. +Developers need to be careful when passing on pagination or limit parameters to the remote API. If _all_ of the parameters cannot be passed through, developers should exclude limits from any query against the remote API (i.e., don't tranlate `resultRecordCount` to it's remote API equivalent). This is because a limit parameter might truncate the result set _before_ all the filters are applied. As an illustration, consider this scenario. + +1. A request arrives with the following parameters `where=country='USA'`, `geometry={"xmin":-120,"ymin":48,"xmax":-119,"ymax":49}`, and `resultRecordCount=100`. +2. The remote API supports filtering by attribute, but not by geometry. +3. The remote API supports truncating the result set with a `limit` parameter +4. The code is the provider sends the request on to the remote API, translating the `where` and the `resultRecordCount` parameters. +5. The remote API returns the first 100 results where attribute `country` is "USA", but only some of these records lie inside the geometry envelope noted above. There are more records that do exist inside this envelope, but the remote has no way to filter or ensure they are included in the first 100 records returned. +6. The results are passed on to Koop, which then applies the geometry filter; this removes 70 records that lie outside the requested geometry envelope. +7. Koop responds to the client with a result set of only 30 records, even though many more exist that fit the requested attribute and geometry parameters. + +As you can see, the response is missing important features, because the result set was truncated _before_ all filters were applied. Developers should be mindful of this anti-pattern when create providers that pass off filtering/sorting to their remote APIs. + +#### `filtersApplied` If you do use the pass-through pattern, it is important that you add the `filtersApplied` object to the GeoJSON delivered by your provider. The object should have a boolean flag for any Geoservices operation that has already been executed on the GeoJSON. ```js diff --git a/_docs/development/authorization.md b/_docs/development/authorization.md index a0fbc59..c732c2d 100644 --- a/_docs/development/authorization.md +++ b/_docs/development/authorization.md @@ -1,5 +1,5 @@ --- -title: Authorization Specification +title: Authorization Plugin Specification permalink: /docs/development/authorization --- @@ -10,9 +10,10 @@ Each authorization plugin must have a file called `index.js`. `index.js` must b ```js { type: 'auth', - authenticationSpecification: Function, authenticate: Function, - authorize: Function + authorize: Function, + name: 'my-auth-plugin', + version: require('../path/to/package.json').version } ``` @@ -23,16 +24,13 @@ The table below contains a brief description of the registration object. Note th | property | type | description | | --- | --- | --- | |`type`| String | Must be set to `auth`; identifies the plugin as an authorization plugin during registration | -|`authenticationSpecification`| Function | Returns an object with data useful for configuring the authorization plugin with output plugins.| |`authorize`| Function | Verfies a user has authorization to make a requests (e.g., a token is validated) | |`authenticate`|Function| Authenticates a user's requests based on submitted input (credentials, key, etc)| +|`name`|String| Name of the plugin | +|`version`|String| Plugin version number | Details about each of the API functions are found below. -### `authenticationSpecification` **function() ⇒ object** - -Authorization plugins must include a function called "authenticationSpecification". Its purpose is delivery of an object (i.e., the _authentication specification_) that provides options to the output-plugin. The object returned need only contain data for properly configuring your output plugins of choice. For example, Koop's default geoservices uses a `useHttp` option when generating the [authentication endpoint](https://github.com/koopjs/koop-output-geoservices/blob/master/index.js#L54). An example of `authenticationSpecification` is available [here](https://github.com/koopjs/koop-auth-direct-file/blob/master/src/index.js#L44-L56). - ### `authenticate` **authenticate(req) ⇒ Promise** | Param | Type | Description | @@ -56,4 +54,4 @@ Authorization plugins are free to validate credentials in any manner. For examp | --- | --- | --- | | req | object | Express request object. Query parameter or header should include input (e.g., token) that can be used to prove previously successful authentication | -Authorization plugins are required to implement a function called `authorize`. It should accept the Express request object as an argument, which that can be used to verify the request is being made by an authenticated user (e.g., validate a token in the authorization header). If the authorization is unsuccessful, the promise should reject with an error object that contains a `401` code. Successful authorization should allow the promise to resolve. An example of an `authorize` function can be viewed [here](https://github.com/koopjs/koop-auth-direct-file/blob/master/src/index.js#L90-L108). +Authorization plugins are required to implement a function called `authorize`. It should accept the Express request object as an argument, which that can be used to verify the request is being made by an authenticated user (e.g., validate a token in the authorization header). If the identity of the request user is unknown or can't be determined (e.g., due to a missing or invalid token) the promise should reject with an error object that contains a `401` code. If the token is valid, but the user is not authorized to access the requested resource, the promise should be rejected with an error that contains a `403` code. Successful authorization should allow the promise to resolve. An example of an `authorize` function can be viewed [here](https://github.com/koopjs/koop-auth-direct-file/blob/master/src/index.js#L90-L108). diff --git a/_docs/development/provider/model.md b/_docs/development/provider/model.md index d00971d..e4ce843 100644 --- a/_docs/development/provider/model.md +++ b/_docs/development/provider/model.md @@ -129,6 +129,21 @@ You should add a `metadata` object to the GeoJSON returned from `getData` (see F #### Special considerations for `idField` As noted above `idField` designates which feature attribute should be used as the feature's unique identifier. For ArcGIS clients, having a unique identifier is required, and it must be of type integer and in the range of 0 - 2,147,483,647, otherwise some functionality cannot be supported. If you chose not to set `idField` or don't have a feature attribute that fits its requirements, Koop will create an OBJECTID field in its Geoservices output by default which can be used as a unique identifier. Its value is calculated as the numeric hash of each feature. While you can let Koop handle this, define an `idField` in your metadata is overall more reliable. +## Method: `getStream(request)` +The optional `getStream` method should return a Readable-Stream of GeoJSON and is leveraged by any output-plugins that invoke the `pullStream` model method. Note that as of this writing few output-plugins leverage this method. + +### Arguments + +| name | type | description | +| - | - | - | +|`request`| Object | An Express `request` object. Contains route and query parameters for use in building the URL to the remote API. See the [Express documentation](https://expressjs.com/en/4x/api.html#req) for more details. | + +## Method: `authorize(request)` +The optional `authorize` method may be defined on a provider's model class. This method will invoked before any invocations of `getData` or `getStream`. It overrides any `authorize` method of a registered authorization-plugin. This methods spec is identical to the [authorize method of a authorization-plugin](docs/development/authorization#authorize-function-authorizereq--promise). + +## Method: `authenticate(request)` +The optional `authenticate` method may be defined on a provider's model class. This method may be use by some output-plugins that have routes that allow token generation. It overrides any `authenticate` method of a registered authorization-plugin. This methods spec is identical to the [authenticate method of a authorization-plugin](docs/development/authorization#authenticate-authenticatereq--promise). + ## Method: `createKey(request)` Koop uses a an internal `createKey` function to generate a string for use as a key in the cache-plugin's key-value store. Koop's `createKey` uses the provider name and route parameters to define a key. This allows all requests with the same provider name and route parameters to leverage cached data. @@ -164,3 +179,4 @@ Model.prototype.createKey = function (request) { } ```
Figure 4. Defining a custom createKey method for generating cache keys.
+ diff --git a/_docs/usage/authorization.md b/_docs/usage/authorization.md index 0ad038a..dab8089 100644 --- a/_docs/usage/authorization.md +++ b/_docs/usage/authorization.md @@ -5,15 +5,18 @@ permalink: /docs/usage/authorization ## Overview -Authorization plugins add a security layer to Koop. There are few important facts to understand when using an authorization plugin: +Authorization plugins or provider methods add a security layer to Koop. There are some important facts to understand when working with authorization on a Koop instance: -* An authorization plugin is only effective for securing routes generated by output service plugins (a.k.a., _plugin-routes_). Custom routes defined by the provider will not be secured by registering an authorization plugin. +* Authorization can be implemented on individual providers or across multiple providers with a Koop authorization plugin. Plugin implementation is useful if authorization rules do not vary by provider. -* Successfully securing your plugin-routes depends on authorization support from any output-plugins you may be using. By default, Koop includes [koop-output-geoservices](https://github.com/koopjs/koop-output-geoservices) (a.k.a. feature service output), which supports securing services with authorization plugins. +* Providers may implement authorization by defining `authorize` and `authenticate` methods on the model class. These methods will override those (`authorize` and `authenticate` methods) defined by any authorization plugin. -* Authorization plugins should be registered _after_ output-services plugins and _before_ providers. Any providers registered before an authorization plugin will not have its plugin routes secured. +* Authorization plugin define `authorize` and `authenticate` methods which are bound to a provider's model class if that class does have such methods already defined. Therefore, registration order is important. For a provider to use a authorization plugins methods, it must be registered _after_ the auth-plugin registration. -## Usage +* An authorization method (whether provider or plugin) is only effective for securing routes generated by output service plugins (a.k.a., _plugin-routes_) that leverage a provider-model's "pull" methods (`pull`, `pullLayer`, `pullCatalog`, or `pullStream`). Custom routes defined by the provider will only be secured if the (1) use a model's pull method or (2) invoke the model's `authorization` modelnot be secured by registering an authorization plugin. + + +## Authorization-Plugin Usage Usage of an authorization-plugin can be conceptually divided into three parts: (1) configuration, (2) registration with Koop, and (3) use in output-services. An example using [koop-auth-direct-file](https://github.com/koopjs/koop-auth-direct-file) provides the best illustration of usage. @@ -42,7 +45,7 @@ koop.register(s3Select) ## Plugin registration order The order in which you register your providers and authorization plugins will affect functionality. The key points are: -* Providers registered *before* the authorization plugin *will not* be secured -* Providers registered *after* the authorization plugin *will* be secured +* Providers registered *before* the authorization plugin *will not* be secured unless they define their own `authorize` method +* Providers registered *after* the authorization plugin *will* be secured by (1) their own `authorize` method if defined on model class or (2) the authorization plugin's `authorize` method. -In the above implementation, the authorization plugin would be applied to feature server routes for the S3 Select provider. Note that the routes for the Github provider _would not_ be protected because the auth plugin registration occurs _after_ the Github provider registration. +In the above implementation, the authorization plugin would be applied to feature server routes for the S3 Select provider. Note that the routes for the Github provider _would not_ be protected because the auth plugin registration occurs _after_ the Github provider registration and its model does not define an `authorize` method..