Skip to content

Commit

Permalink
feat: add authentication extension
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Jun 20, 2024
1 parent e5ce39a commit 6731baf
Show file tree
Hide file tree
Showing 4 changed files with 390 additions and 0 deletions.
128 changes: 128 additions & 0 deletions stac/examples/auth/collection.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
85 changes: 85 additions & 0 deletions stac/examples/auth/item.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
175 changes: 175 additions & 0 deletions stac/src/extensions/authentication.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! 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 std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;

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<String, Scheme>,

/// 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<String>,
}

/// 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<String>,

/// The name of the header, query, or cookie parameter to be used.
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,

/// The location of the API key (`query` | `header` | `cookie`).
#[serde(skip_serializing_if = "Option::is_none")]
pub r#in: Option<String>,

/// 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<String>,

/// 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<String, Flow>,

/// 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<String>,
}

/// 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<String>,

/// 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<String>,

/// 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<String, String>,

/// 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<String>,
},

/// 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<String>,

/// Parameter definition for requests to the authorizationApi
#[serde(skip_serializing_if = "HashMap::is_empty")]
parameters: HashMap<String, Parameter>,

/// Key name for the signed URL field in an authorizationApi response
#[serde(skip_serializing_if = "Option::is_none", rename = "responseField")]
response_field: Option<String>,
}
}

/// 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<String>,

/// Schema object following the [JSON Schema draft-07](Schema object following the JSON Schema draft-07).
pub schema: HashMap<String, Value>,
}

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 crate::{Collection, Item, Extensions};
use super::Authentication;

#[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();
}
}
Loading

0 comments on commit 6731baf

Please sign in to comment.