Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ pub enum KeystoneApiError {
#[error("missing authorization")]
Unauthorized(String),

#[error("missing x-subject-token header")]
SubjectTokenMissing,

#[error("invalid header")]
InvalidHeader,

#[error("invalid token")]
InvalidToken,

#[error("error building token data: {}", source)]
TokenBuilder {
#[from]
source: crate::api::v3::auth::token::types::TokenBuilderError,
},

#[error("internal server error")]
InternalError(String),

Expand Down Expand Up @@ -74,6 +89,11 @@ impl IntoResponse for KeystoneApiError {
Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})),
).into_response()
}
KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::TokenBuilder{..} => {
(StatusCode::BAD_REQUEST,
Json(json!({"error": {"code": StatusCode::BAD_REQUEST.as_u16(), "message": self.to_string()}})),
).into_response()
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/api/v3/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 utoipa_axum::router::OpenApiRouter;

use crate::keystone::ServiceState;

pub mod token;

pub(super) fn openapi_router() -> OpenApiRouter<ServiceState> {
OpenApiRouter::new().nest("/tokens", token::openapi_router())
}
143 changes: 143 additions & 0 deletions src/api/v3/auth/token/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// 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::{extract::State, http::HeaderMap, response::IntoResponse};
use utoipa_axum::{router::OpenApiRouter, routes};

use crate::api::auth::Auth;
use crate::api::error::KeystoneApiError;
use crate::keystone::ServiceState;
use crate::token::{TokenApi, types::TokenData};
use types::{TokenBuilder, TokenResponse};

pub mod types;

pub(super) fn openapi_router() -> OpenApiRouter<ServiceState> {
OpenApiRouter::new().routes(routes!(validate))
}

/// Validate token
#[utoipa::path(
get,
path = "/",
description = "Validate token",
params(),
responses(
(status = OK, description = "Token object", body = TokenResponse),
),
tag="auth"
)]
#[tracing::instrument(name = "api::token_get", level = "debug", skip(state))]
async fn validate(
Auth(user_auth): Auth,
headers: HeaderMap,
State(state): State<ServiceState>,
) -> Result<impl IntoResponse, KeystoneApiError> {
let subject_token: String = headers
.get("X-Subject-Token")
.ok_or(KeystoneApiError::SubjectTokenMissing)?
.to_str()
.map_err(|_| KeystoneApiError::InvalidHeader)?
.to_string();

let token = state
.provider
.get_token_provider()
.validate_token(subject_token, None)
.await
.map_err(|_| KeystoneApiError::InvalidToken)?;

let mut response = TokenBuilder::default();
response.audit_ids(token.audit_ids().clone());
response.methods(token.methods().clone());
response.expires_at(*token.expires_at());
Ok(TokenResponse {
token: response.build()?,
})
}

#[cfg(test)]
mod tests {
use axum::{
body::Body,
http::{Request, StatusCode},
};
use http_body_util::BodyExt; // for `collect`
use tower::ServiceExt; // for `call`, `oneshot`, and `ready`
use tower_http::trace::TraceLayer;

use super::openapi_router;
use crate::api::v3::auth::token::types::TokenResponse;
use crate::identity::MockIdentityProvider;
use crate::tests::api::{get_mocked_state, get_mocked_state_unauthed};

#[tokio::test]
async fn test_get() {
let identity_mock = MockIdentityProvider::default();
let state = get_mocked_state(identity_mock);

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
.with_state(state.clone());

let response = api
.as_service()
.oneshot(
Request::builder()
.uri("/")
.header("x-auth-token", "foo")
.header("x-subject-token", "foo")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::OK);

let body = response.into_body().collect().await.unwrap().to_bytes();
let _res: TokenResponse = serde_json::from_slice(&body).unwrap();

let response = api
.as_service()
.oneshot(
Request::builder()
.uri("/")
.header("x-auth-token", "foo")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn test_get_unauth() {
let state = get_mocked_state_unauthed();

let mut api = openapi_router()
.layer(TraceLayer::new_for_http())
.with_state(state);

let response = api
.as_service()
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();

assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}
78 changes: 78 additions & 0 deletions src/api/v3/auth/token/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 derive_builder::Builder;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Authorization token
#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
#[builder(setter(strip_option, into))]
pub struct Token {
/// A list of one or two audit IDs. An audit ID is a unique, randomly generated, URL-safe
/// string that you can use to track a token. The first audit ID is the current audit ID for
/// the token. The second audit ID is present for only re-scoped tokens and is the audit ID
/// from the token before it was re-scoped. A re- scoped token is one that was exchanged for
/// another token of the same or different scope. You can use these audit IDs to track the use
/// of a token or chain of tokens across multiple requests and endpoints without exposing the
/// token ID to non-privileged users.
pub audit_ids: Vec<String>,

/// The authentication methods, which are commonly password, token, or other methods. Indicates
/// the accumulated set of authentication methods that were used to obtain the token. For
/// example, if the token was obtained by password authentication, it contains password. Later,
/// if the token is exchanged by using the token authentication method one or more times, the
/// subsequently created tokens contain both password and token in their methods attribute.
/// Unlike multi-factor authentication, the methods attribute merely indicates the methods that
/// were used to authenticate the user in exchange for a token. The client is responsible for
/// determining the total number of authentication factors.
pub methods: Vec<String>,

/// The date and time when the token expires.
pub expires_at: DateTime<Utc>,

/// A project object including the id, name and domain object representing the project the
/// token is scoped to. This is only included in tokens that are scoped to a project.
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(default)]
pub project: Option<Project>,
}

#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
#[builder(setter(strip_option, into))]
pub struct TokenResponse {
/// Token
pub token: Token,
}

impl IntoResponse for TokenResponse {
fn into_response(self) -> Response {
(StatusCode::OK, Json(self)).into_response()
}
}

/// Project information
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)]
pub struct Project {
/// Project ID
id: String,
/// Project Name
name: String,
}
4 changes: 3 additions & 1 deletion src/api/v3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ use utoipa_axum::router::OpenApiRouter;

use crate::keystone::ServiceState;

pub mod auth;
pub mod group;
pub mod user;

pub(super) fn openapi_router() -> OpenApiRouter<ServiceState> {
OpenApiRouter::new()
.nest("/users", user::openapi_router())
.nest("/auth", auth::openapi_router())
.nest("/groups", group::openapi_router())
.nest("/users", user::openapi_router())
}
1 change: 1 addition & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
// SPDX-License-Identifier: Apache-2.0

pub(crate) mod api;
pub(crate) mod token;
4 changes: 2 additions & 2 deletions src/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::config::Config;
use crate::identity::MockIdentityProvider;
use crate::keystone::{Service, ServiceState};
use crate::provider::ProviderBuilder;
use crate::token::{MockTokenProvider, Token, TokenProviderError};
use crate::token::{MockTokenProvider, Token, TokenProviderError, UnscopedToken};

pub(crate) fn get_mocked_state_unauthed() -> ServiceState {
let db = DatabaseConnection::Disconnected;
Expand All @@ -46,7 +46,7 @@ pub(crate) fn get_mocked_state(identity_mock: MockIdentityProvider) -> ServiceSt
let mut token_mock = MockTokenProvider::default();
token_mock
.expect_validate_token()
.returning(|_, _| Ok(Token::default()));
.returning(|_, _| Ok(Token::Unscoped(UnscopedToken::default())));

let provider = ProviderBuilder::default()
.config(config.clone())
Expand Down
37 changes: 37 additions & 0 deletions src/tests/token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::tempdir;

use crate::config::Config;

pub fn setup_config() -> Config {
let keys_dir = tempdir().unwrap();
// write fernet key used to generate tokens in python
let file_path = keys_dir.path().join("0");
let mut tmp_file = File::create(file_path).unwrap();
write!(tmp_file, "BFTs1CIVIBLTP4GOrQ26VETrJ7Zwz1O4wbEcCQ966eM=").unwrap();
let mut config = Config::new(PathBuf::new()).unwrap();
config.fernet_tokens.key_repository = keys_dir.into_path();
config.auth.methods = vec![
"password".into(),
"token".into(),
"openid".into(),
"application_credential".into(),
];
config
}
Loading