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
2 changes: 1 addition & 1 deletion src/api/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ where
state
.provider
.get_token_provider()
.validate_token(auth_header, None)
.validate_token(auth_header, Some(false), None)
.await
.map_err(|_| (StatusCode::UNAUTHORIZED, "not authorized"))?,
))
Expand Down
2 changes: 1 addition & 1 deletion src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub enum KeystoneApiError {
#[error("missing x-subject-token header")]
SubjectTokenMissing,

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

#[error("invalid token")]
Expand Down
160 changes: 155 additions & 5 deletions src/api/v3/auth/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ use crate::resource::{
types::{Domain, Project},
};
use crate::token::TokenApi;
use types::{AuthRequest, CreateTokenParameters, Scope, Token as ApiResponseToken, TokenResponse};
use types::{
AuthRequest, CreateTokenParameters, Scope, Token as ApiResponseToken, TokenResponse,
ValidateTokenParameters,
};

mod common;
pub mod types;
Expand Down Expand Up @@ -206,7 +209,7 @@ async fn post(
get,
path = "/",
description = "Validate token",
params(),
params(ValidateTokenParameters),
responses(
(status = OK, description = "Token object", body = TokenResponse),
),
Expand All @@ -219,6 +222,7 @@ async fn post(
)]
async fn show(
Auth(_user_auth): Auth,
Query(query): Query<ValidateTokenParameters>,
headers: HeaderMap,
State(state): State<ServiceState>,
) -> Result<impl IntoResponse, KeystoneApiError> {
Expand All @@ -232,9 +236,12 @@ async fn show(
let mut token = state
.provider
.get_token_provider()
.validate_token(&subject_token, None)
.validate_token(&subject_token, query.allow_expired, None)
.await
.map_err(|_| KeystoneApiError::InvalidToken)?;
.map_err(|_| KeystoneApiError::NotFound {
resource: "token".into(),
identifier: String::new(),
})?;

state
.provider
Expand Down Expand Up @@ -315,7 +322,7 @@ mod tests {
}))
});
let mut token_mock = MockTokenProvider::default();
token_mock.expect_validate_token().returning(|_, _| {
token_mock.expect_validate_token().returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
Expand Down Expand Up @@ -380,6 +387,149 @@ mod tests {
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn test_get_allow_expired() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
let mut identity_mock = MockIdentityProvider::default();
identity_mock.expect_get_user().returning(|_, id: &'_ str| {
Ok(Some(UserResponse {
id: id.to_string(),
domain_id: "user_domain_id".into(),
..Default::default()
}))
});

let mut resource_mock = MockResourceProvider::default();
resource_mock
.expect_get_domain()
.withf(|_: &DatabaseConnection, id: &'_ str| id == "user_domain_id")
.returning(|_, _| {
Ok(Some(Domain {
id: "user_domain_id".into(),
..Default::default()
}))
});
let mut token_mock = MockTokenProvider::default();
token_mock
.expect_validate_token()
.withf(|token: &'_ str, _, _| token == "foo")
.returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
}))
});
token_mock
.expect_validate_token()
.withf(|token: &'_ str, allow_expired: &Option<bool>, _| {
token == "bar" && *allow_expired == Some(true)
})
.returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
}))
});
token_mock
.expect_populate_role_assignments()
.returning(|_, _, _| Ok(()));
token_mock
.expect_expand_project_information()
.returning(|_, _, _| Ok(()));
token_mock
.expect_expand_domain_information()
.returning(|_, _, _| Ok(()));

let provider = ProviderBuilder::default()
.config(config.clone())
.assignment(assignment_mock)
.catalog(catalog_mock)
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

let state = Arc::new(Service::new(config, db, provider).unwrap());

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

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

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

#[tokio::test]
async fn test_get_expired() {
let db = DatabaseConnection::Disconnected;
let config = Config::default();
let assignment_mock = MockAssignmentProvider::default();
let catalog_mock = MockCatalogProvider::default();
let identity_mock = MockIdentityProvider::default();
let resource_mock = MockResourceProvider::default();
let mut token_mock = MockTokenProvider::default();
token_mock
.expect_validate_token()
.withf(|token: &'_ str, _, _| token == "foo")
.returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
}))
});
token_mock
.expect_validate_token()
.withf(|token: &'_ str, _, _| token == "bar")
.returning(|_, _, _| Err(TokenProviderError::Expired));

let provider = ProviderBuilder::default()
.config(config.clone())
.assignment(assignment_mock)
.catalog(catalog_mock)
.identity(identity_mock)
.resource(resource_mock)
.token(token_mock)
.build()
.unwrap();

let state = Arc::new(Service::new(config, db, provider).unwrap());

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", "bar")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();

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

#[tokio::test]
async fn test_get_unauth() {
let state = get_mocked_state_unauthed();
Expand Down
9 changes: 9 additions & 0 deletions src/api/v3/auth/token/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,12 @@ pub struct CreateTokenParameters {
/// the service catalog.
pub nocatalog: Option<bool>,
}

#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)]
pub struct ValidateTokenParameters {
/// The authentication response excludes the service catalog. By default, the response includes
/// the service catalog.
pub nocatalog: Option<bool>,
/// Allow fetching a token that has expired. By default expired tokens return a 404 exception.
pub allow_expired: Option<bool>,
}
2 changes: 1 addition & 1 deletion src/api/v3/role/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ mod tests {
let config = Config::default();
let mut token_mock = MockTokenProvider::default();
let resource_mock = MockResourceProvider::default();
token_mock.expect_validate_token().returning(|_, _| {
token_mock.expect_validate_token().returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
Expand Down
2 changes: 1 addition & 1 deletion src/api/v3/role_assignment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ mod tests {
let config = Config::default();
let mut token_mock = MockTokenProvider::default();
let resource_mock = MockResourceProvider::default();
token_mock.expect_validate_token().returning(|_, _| {
token_mock.expect_validate_token().returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
Expand Down
5 changes: 4 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ pub struct Config {
}

#[derive(Debug, Default, Deserialize, Clone)]
pub struct DefaultSection {}
pub struct DefaultSection {
/// Debug logging
pub debug: Option<bool>,
}

#[derive(Debug, Default, Deserialize, Clone)]
pub struct AuthSection {
Expand Down
4 changes: 2 additions & 2 deletions src/tests/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub(crate) fn get_mocked_state_unauthed() -> ServiceState {
let mut token_mock = MockTokenProvider::default();
token_mock
.expect_validate_token()
.returning(|_, _| Err(TokenProviderError::InvalidToken));
.returning(|_, _, _| Err(TokenProviderError::InvalidToken));

let provider = ProviderBuilder::default()
.config(config.clone())
Expand All @@ -54,7 +54,7 @@ pub(crate) fn get_mocked_state(identity_mock: MockIdentityProvider) -> ServiceSt
let config = Config::default();
let mut token_mock = MockTokenProvider::default();
let resource_mock = MockResourceProvider::default();
token_mock.expect_validate_token().returning(|_, _| {
token_mock.expect_validate_token().returning(|_, _, _| {
Ok(Token::Unscoped(UnscopedToken {
user_id: "bar".into(),
..Default::default()
Expand Down
4 changes: 4 additions & 0 deletions src/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub trait TokenApi: Send + Sync + Clone {
async fn validate_token<'a>(
&self,
credential: &'a str,
allow_expired: Option<bool>,
window_seconds: Option<i64>,
) -> Result<Token, TokenProviderError>;

Expand Down Expand Up @@ -122,6 +123,7 @@ impl TokenApi for TokenProvider {
async fn validate_token<'a>(
&self,
credential: &'a str,
allow_expired: Option<bool>,
window_seconds: Option<i64>,
) -> Result<Token, TokenProviderError> {
let token = self.backend_driver.decode(credential)?;
Expand All @@ -130,6 +132,7 @@ impl TokenApi for TokenProvider {
.expires_at()
.checked_add_signed(TimeDelta::seconds(window_seconds.unwrap_or(0)))
.unwrap_or(*token.expires_at())
&& !allow_expired.unwrap_or(false)
{
return Err(TokenProviderError::Expired);
}
Expand Down Expand Up @@ -351,6 +354,7 @@ mock! {
async fn validate_token<'a>(
&self,
credential: &'a str,
allow_expired: Option<bool>,
window_seconds: Option<i64>,
) -> Result<Token, TokenProviderError>;

Expand Down