diff --git a/src/api/mod.rs b/src/api/mod.rs index d0d0f90e..1957e728 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -12,19 +12,68 @@ // // SPDX-License-Identifier: Apache-2.0 +use axum::{ + extract::OriginalUri, + http::{HeaderMap, header}, + response::IntoResponse, +}; use utoipa::OpenApi; -use utoipa_axum::router::OpenApiRouter; +use utoipa_axum::{router::OpenApiRouter, routes}; +use crate::api::error::KeystoneApiError; use crate::keystone::ServiceState; pub mod auth; pub mod error; +pub mod types; pub mod v3; +use crate::api::types::*; + #[derive(OpenApi)] #[openapi(info(version = "3.14.0"))] pub struct ApiDoc; pub fn openapi_router() -> OpenApiRouter { - OpenApiRouter::new().nest("/v3", v3::openapi_router()) + OpenApiRouter::new() + .nest("/v3", v3::openapi_router()) + .routes(routes!(version)) +} + +/// Versions +#[utoipa::path( + get, + path = "/", + description = "Version discovery", + responses( + (status = OK, description = "Versions", body = Versions), + ), + tag = "version" +)] +async fn version( + headers: HeaderMap, + OriginalUri(uri): OriginalUri, +) -> Result { + let host = headers + .get(header::HOST) + .and_then(|header| header.to_str().ok()) + .unwrap_or("localhost"); + + let link = Link { + rel: "self".into(), + href: format!("http://{}/v3", host), + }; + let version = Version { + id: "v3.14".into(), + status: VersionStatus::Stable, + links: Some(vec![link]), + media_types: Some(vec![MediaType::default()]), + ..Default::default() + }; + let res = Versions { + versions: Values { + values: vec![version], + }, + }; + Ok(res) } diff --git a/src/api/types.rs b/src/api/types.rs new file mode 100644 index 00000000..963a20f3 --- /dev/null +++ b/src/api/types.rs @@ -0,0 +1,89 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Versions { + pub versions: Values, +} + +impl IntoResponse for Versions { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Values { + pub values: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct SingleVersion { + pub version: Version, +} + +impl IntoResponse for SingleVersion { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Version { + pub id: String, + pub status: VersionStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub updated: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub links: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub media_types: Option>, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum VersionStatus { + #[default] + #[serde(rename = "stable")] + Stable, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Link { + pub rel: String, + pub href: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct MediaType { + pub base: String, + pub r#type: String, +} + +impl Default for MediaType { + fn default() -> Self { + Self { + base: "application/json".into(), + r#type: "application/vnd.openstack.identity-v3+json".into(), + } + } +} diff --git a/src/api/v3/mod.rs b/src/api/v3/mod.rs index ea65b54e..4c81e599 100644 --- a/src/api/v3/mod.rs +++ b/src/api/v3/mod.rs @@ -12,8 +12,14 @@ // // SPDX-License-Identifier: Apache-2.0 -use utoipa_axum::router::OpenApiRouter; +use axum::{ + extract::{OriginalUri, Request}, + http::{HeaderMap, header}, + response::IntoResponse, +}; +use utoipa_axum::{router::OpenApiRouter, routes}; +use crate::api::error::KeystoneApiError; use crate::keystone::ServiceState; pub mod auth; @@ -22,6 +28,8 @@ pub mod role; pub mod role_assignment; pub mod user; +use crate::api::types::*; + pub(super) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new() .nest("/auth", auth::openapi_router()) @@ -29,4 +37,40 @@ pub(super) fn openapi_router() -> OpenApiRouter { .nest("/role_assignments", role_assignment::openapi_router()) .nest("/roles", role::openapi_router()) .nest("/users", user::openapi_router()) + .routes(routes!(version)) +} + +/// Version +#[utoipa::path( + get, + path = "/", + description = "Version discovery", + responses( + (status = OK, description = "Versions", body = SingleVersion), + ), + tag = "version" +)] +async fn version( + headers: HeaderMap, + OriginalUri(uri): OriginalUri, + req: Request, +) -> Result { + println!("Request: {:?}, uri: {:?}", req, uri); + let host = headers + .get(header::HOST) + .and_then(|header| header.to_str().ok()) + .unwrap_or("localhost"); + let link = Link { + rel: "self".into(), + href: format!("http://{}{}", host, uri.path()), + }; + let version = Version { + id: "v3.14".into(), + status: VersionStatus::Stable, + links: Some(vec![link]), + media_types: Some(vec![MediaType::default()]), + ..Default::default() + }; + let res = SingleVersion { version }; + Ok(res) } diff --git a/src/config.rs b/src/config.rs index 1394522b..e55a337e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -168,6 +168,7 @@ pub struct TokenSection { #[derive(Debug, Default, Deserialize, Clone)] pub enum TokenProvider { #[default] + #[serde(rename = "fernet")] Fernet, }