diff --git a/stac/CHANGELOG.md b/stac/CHANGELOG.md index d09b385a..491d8ad3 100644 --- a/stac/CHANGELOG.md +++ b/stac/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - `stac::datetime::Interval` ([#252](https://github.com/stac-utils/stac-rs/pull/252)) - `TryFrom` and `TryInto` for `Item` ([#255](https://github.com/stac-utils/stac-rs/pull/255)) - `wkb` feature and `GeoparquetItem` ([#260](https://github.com/stac-utils/stac-rs/pull/260), [#263](https://github.com/stac-utils/stac-rs/pull/263)) +- Authentication extension ([#268](https://github.com/stac-utils/stac-rs/pull/268)) ### Fixed diff --git a/stac/examples/auth/collection.json b/stac/examples/auth/collection.json new file mode 100644 index 00000000..42d8352b --- /dev/null +++ b/stac/examples/auth/collection.json @@ -0,0 +1,128 @@ +{ + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/item-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json" + ], + "type": "Collection", + "id": "collection", + "title": "A title", + "description": "A description", + "license": "Apache-2.0", + "extent": { + "spatial": { + "bbox": [ + [ + 172.9, + 1.3, + 173, + 1.4 + ] + ] + }, + "temporal": { + "interval": [ + [ + "2015-06-23T00:00:00Z", + null + ] + ] + } + }, + "auth:schemes": { + "oauth": { + "type": "oauth2", + "description": "requires a login and user token", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "read:example": "Read the example data", + "write:example": "Write the example data", + "admin:example": "Read/write/delete the example data" + } + } + } + }, + "signed_url_auth": { + "type": "signedUrl", + "description": "Requires an authentication API", + "flows": { + "auth": { + "authorizationApi": "https://example.com/signed_url/authorize", + "method": "POST", + "parameters": { + "bucket": { + "in": "body", + "required": true, + "description": "asset-bucket", + "schema": { + "type": "string", + "examples": [ + "example-bucket" + ] + } + }, + "key": { + "in": "body", + "required": true, + "description": "asset key", + "schema": { + "type": "string", + "examples": [ + "path/to/example/asset.xyz" + ] + } + } + }, + "responseField": "signed_url" + } + } + } + }, + "assets": { + "example": { + "href": "https://example.com/examples/file.xyz", + "title": "Secure Collection Asset Example", + "type": "application/vnd.example", + "roles": [ + "data" + ], + "auth:refs": [ + "signed_url_auth" + ] + } + }, + "item_assets": { + "data": { + "title": "Secure Collection Asset Example", + "type": "application/vnd.example", + "roles": [ + "data" + ], + "auth:refs": [ + "oauth" + ] + } + }, + "summaries": { + "datetime": { + "minimum": "2015-06-23T00:00:00Z", + "maximum": "2019-07-10T13:44:56Z" + } + }, + "links": [ + { + "href": "https://example.com/examples/collection.json", + "rel": "self" + }, + { + "href": "https://example.com/examples/item.json", + "rel": "item", + "auth:refs": [ + "oauth" + ] + } + ] +} \ No newline at end of file diff --git a/stac/examples/auth/item.json b/stac/examples/auth/item.json new file mode 100644 index 00000000..5e189e1a --- /dev/null +++ b/stac/examples/auth/item.json @@ -0,0 +1,85 @@ +{ + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json" + ], + "type": "Feature", + "id": "item", + "bbox": [ + 172.9, + 1.3, + 173, + 1.4 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 172.9, + 1.3 + ], + [ + 173, + 1.3 + ], + [ + 173, + 1.4 + ], + [ + 172.9, + 1.4 + ], + [ + 172.9, + 1.3 + ] + ] + ] + }, + "properties": { + "datetime": "2020-12-11T22:38:32Z", + "auth:schemes": { + "oauth": { + "type": "oauth2", + "description": "requires a login and user token", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "read:example": "Read the example data", + "write:example": "Write the example data", + "admin:example": "Read/write/delete the example data" + } + } + } + }, + "none": { + "type": "http", + "scheme": "basic", + "description": "Free access without restrictions" + } + } + }, + "links": [ + { + "href": "https://example.com/examples/item.json", + "rel": "self" + } + ], + "assets": { + "data": { + "href": "https://example.com/examples/file.xyz", + "title": "Secure Asset Example", + "type": "application/vnd.example", + "roles": [ + "data" + ], + "auth:refs": [ + "oauth" + ] + } + } +} diff --git a/stac/src/extensions/authentication.rs b/stac/src/extensions/authentication.rs new file mode 100644 index 00000000..eb144d31 --- /dev/null +++ b/stac/src/extensions/authentication.rs @@ -0,0 +1,204 @@ +//! The Authentication extension to the STAC specification provides a standard +//! set of fields to describe authentication and authorization schemes, flows, +//! and scopes required to access [Assets](crate::Asset) and +//! [Links](crate::Link) that align with the [OpenAPI security +//! spec](https://swagger.io/docs/specification/authentication/). + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::Extension; + +/// The authentication extension fields. +#[derive(Debug, Serialize, Deserialize)] +pub struct Authentication { + /// A property that contains all of the [scheme definitions](Scheme) used by + /// [Assets](crate::Asset) and [Links](crate::Link) in the STAC [Item](crate::Item) or [Collection](crate::Collection). + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub schemes: HashMap, + + /// A property that specifies which schemes may be used to access an [Asset](crate::Asset) + /// or [Link](crate::Link). + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub refs: Vec, +} + +/// The Authentication Scheme extends the [OpenAPI security +/// spec](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object) +/// for support of OAuth2.0, API Key, and OpenID Connect authentication. +#[derive(Debug, Serialize, Deserialize)] +pub struct Scheme { + /// The authentication scheme type used to access the data (`http` | `s3` | + /// `signedUrl` | `oauth2` | `apiKey` | `openIdConnect` | a custom scheme type ). + pub r#type: String, + + /// Additional instructions for authentication. + /// + /// [CommonMark 0.29](https://commonmark.org/) syntax MAY be used for rich text representation. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// The name of the header, query, or cookie parameter to be used. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// The location of the API key (`query` | `header` | `cookie`). + #[serde(skip_serializing_if = "Option::is_none")] + pub r#in: Option, + + /// The name of the HTTP Authorization scheme to be used in the Authorization header as defined in RFC7235. + /// + /// The values used SHOULD be registered in the IANA Authentication Scheme registry. + /// (`basic` | `bearer` | `digest` | `dpop` | `hoba` | `mutual` | + /// `negotiate` | `oauth` (1.0) | `privatetoken` | `scram-sha-1` | + /// `scram-sha-256` | `vapid`) + #[serde(skip_serializing_if = "Option::is_none")] + pub scheme: Option, + + /// Scenarios an API client performs to get an access token from the authorization server. + /// + /// For oauth2 the following keys are pre-defined for the corresponding + /// OAuth flows: `authorizationCode` | `implicit` | `password` | + /// `clientCredentials`. The OAuth2 Flow Object applies for oauth2, the + /// Signed URL Object applies to signedUrl. + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + pub flows: HashMap, + + /// OpenID Connect URL to discover OpenID configuration values. + /// + /// This MUST be in the form of a URL. + #[serde(skip_serializing_if = "Option::is_none", rename = "openIdConnectUrl")] + pub open_id_connect_url: Option, +} + +/// The OAuth2 Flow Object applies for oauth2, the Signed URL Object applies to signedUrl. +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Flow { + /// Based on the [OpenAPI OAuth Flow + /// Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object). + /// + /// Allows configuration of the supported OAuth Flows. + OAuth2 { + /// The authorization URL to be used for this flow. + /// + /// This MUST be in the form of a URL. + #[serde(skip_serializing_if = "Option::is_none", rename = "authorizationUrl")] + authorization_url: Option, + + /// The token URL to be used for this flow. + /// + /// This MUST be in the form of a URL. + #[serde(skip_serializing_if = "Option::is_none", rename = "tokenUrl")] + token_url: Option, + + /// The available scopes for the authentication scheme. + /// + /// A map between the scope name and a short description for it. The map MAY be empty. + scopes: HashMap, + + /// The URL to be used for obtaining refresh tokens. + /// + /// This MUST be in the form of a URL. + #[serde(skip_serializing_if = "Option::is_none", rename = "refreshUrl")] + refresh_url: Option, + }, + + /// A signed url flow. + SignedUrl { + /// The method to be used for requests. + method: String, + + /// The signed URL API endpoint to be used for this flow. + /// + /// If not inferred from the client environment, this must be defined in the authentication flow. + #[serde(skip_serializing_if = "Option::is_none", rename = "authorizationApi")] + authorization_api: Option, + + /// Parameter definition for requests to the authorizationApi + #[serde(skip_serializing_if = "HashMap::is_empty")] + parameters: HashMap, + + /// Key name for the signed URL field in an authorizationApi response + #[serde(skip_serializing_if = "Option::is_none", rename = "responseField")] + response_field: Option, + }, +} + +/// Definition for a request parameter. +#[derive(Debug, Serialize, Deserialize)] +pub struct Parameter { + /// The location of the parameter (`query` | `header` | `body`). + pub r#in: String, + + /// Setting for optional or required parameter. + pub required: bool, + + /// Plain language description of the parameter + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Schema object following the [JSON Schema draft-07](Schema object following the JSON Schema draft-07). + pub schema: HashMap, +} + +/// Query, header, or cookie. +#[derive(Debug, Serialize, Deserialize, Default, PartialEq)] +pub enum In { + /// In the GET query string. + #[serde(rename = "query")] + Query, + + /// In the headers. + #[default] + #[serde(rename = "header")] + Header, + + /// In the cookie. + #[serde(rename = "cookie")] + Cookie, +} + +impl Extension for Authentication { + const IDENTIFIER: &'static str = + "https://stac-extensions.github.io/authentication/v1.1.0/schema.json"; + const PREFIX: &'static str = "auth"; +} + +#[cfg(test)] +mod tests { + use super::{Authentication, In, Scheme}; + use crate::{Collection, Extensions, Item}; + use serde_json::json; + + #[test] + fn collection() { + let collection: Collection = crate::read("examples/auth/collection.json").unwrap(); + let authentication: Authentication = collection.extension().unwrap().unwrap(); + let oauth = authentication.schemes.get("oauth").unwrap(); + let _ = oauth.flows.get("authorizationCode").unwrap(); + // FIXME: assets should be able to have extensions from their parent item + // let asset = collection.assets.get("example").unwrap(); + // let authentication: Authentication = asset.extension().unwrap().unwrap(); + // assert_eq!(authentication.refs, vec!["signed_url_auth".to_string()]); + } + + #[test] + fn item() { + let collection: Item = crate::read("examples/auth/item.json").unwrap(); + let authentication: Authentication = collection.extension().unwrap().unwrap(); + let _ = authentication.schemes.get("none").unwrap(); + } + + #[test] + fn api_key() { + let scheme: Scheme = serde_json::from_value(json!({ + "type": "apiKey", + "in": "query", + "name": "API_KEY" + })) + .unwrap(); + assert_eq!(scheme.r#in.unwrap(), In::Query); + } +} diff --git a/stac/src/extensions/mod.rs b/stac/src/extensions/mod.rs index 1a014b5b..1ad52054 100644 --- a/stac/src/extensions/mod.rs +++ b/stac/src/extensions/mod.rs @@ -10,6 +10,7 @@ //! //! | Extension | Maturity | **stac-rs** supported version | //! | -- | -- | -- | +//! | [Authentication](https://github.com/stac-extensions/authentication) | Proposal | v1.1.0 | //! | [Electro-Optical](https://github.com/stac-extensions/eo) | Stable | n/a | //! | [File Info](https://github.com/stac-extensions/file) | Stable | n/a | //! | [Landsat](https://github.com/stac-extensions/landsat) | Stable | n/a | @@ -42,6 +43,7 @@ //! assert!(!item.has_extension::()); //! ``` +pub mod authentication; pub mod projection; pub mod raster;