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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/api-types/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub enum Scope {
Domain(Domain),
/// System scope.
System(System),
/// Unscoped
Unscoped,
}

#[cfg(feature = "validate")]
Expand All @@ -42,6 +44,7 @@ impl validator::Validate for Scope {
Self::Project(project) => project.validate(),
Self::Domain(domain) => domain.validate(),
Self::System(system) => system.validate(),
Self::Unscoped => Ok(()),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/api-types/src/scope_conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ impl From<api_types::Scope> for openstack_keystone_core_types::scope::Scope {
api_types::Scope::Project(scope) => Self::Project(scope.into()),
api_types::Scope::Domain(scope) => Self::Domain(scope.into()),
api_types::Scope::System(scope) => Self::System(scope.into()),
api_types::Scope::Unscoped => Self::Unscoped,
}
}
}
2 changes: 1 addition & 1 deletion crates/api-types/src/v3/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ pub struct UserUpdate {
pub struct UserUpdateRequest {
/// User object.
#[cfg_attr(feature = "validate", validate(nested))]
pub user: UserCreate,
pub user: UserUpdate,
}

/// User options.
Expand Down
17 changes: 16 additions & 1 deletion crates/api-types/src/v3/user_conv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,22 @@ impl From<api_types::UserListParameters> for provider_types::UserListParameters
domain_id: value.domain_id,
name: value.name,
unique_id: value.unique_id,
..Default::default() // limit: value.limit,
..Default::default()
}
}
}

impl From<api_types::UserUpdateRequest> for provider_types::UserUpdate {
fn from(value: api_types::UserUpdateRequest) -> Self {
let user = value.user;
Self {
default_project_id: user.default_project_id,
enabled: user.enabled,
extra: user.extra,
federated: None,
name: user.name,
options: user.options.map(Into::into),
password: user.password,
}
}
}
14 changes: 13 additions & 1 deletion crates/config/src/security_compliance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
use chrono::{NaiveDate, TimeDelta, Utc};
use chrono::{DateTime, NaiveDate, TimeDelta, Utc};
use serde::Deserialize;

use crate::common::*;
Expand Down Expand Up @@ -173,6 +173,18 @@ impl SecurityComplianceProvider {
.map(|val| val.date_naive())
})
}

/// Calculate password expiration time.
///
/// # Parameters
/// - `now`: The current time.
///
/// # Returns
/// An `Option` with the expiration date, or `None` if password expiration is not configured.
pub fn get_password_expires_at(&self, now: DateTime<Utc>) -> Option<DateTime<Utc>> {
self.password_expires_days
.map(|days| now + chrono::TimeDelta::days(days as i64))
}
}

struct AccountLockoutDuration {}
Expand Down
3 changes: 3 additions & 0 deletions crates/core-types/src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub enum Scope {
Domain(Domain),
/// System scope.
System(System),
/// Unscoped
Unscoped,
}

/// Project scope information.
Expand Down Expand Up @@ -73,6 +75,7 @@ impl Validate for Scope {
Self::Project(x) => x.validate(),
Self::Domain(x) => x.validate(),
Self::System(x) => x.validate(),
Self::Unscoped => Ok(()),
}
}
}
1 change: 1 addition & 0 deletions crates/core/src/api/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub async fn get_authz_info(
}
Some(ProviderScope::System(_scope)) => ScopeInfo::System("system".into()),
// TODO: Trust scope should be handled here
Some(ProviderScope::Unscoped) => ScopeInfo::Unscoped,
None => ScopeInfo::Unscoped,
};
authz_scope.validate()?;
Expand Down
13 changes: 13 additions & 0 deletions crates/core/src/identity/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,19 @@ pub trait IdentityBackend: Send + Sync {
group_ids: HashSet<&'a str>,
) -> Result<(), IdentityProviderError>;

/// Update user.
///
/// # Parameters
/// - `state`: The service state.
/// - `user_id`: The ID of the user to update.
/// - `user`: The user details to update.
async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError>;

/// Set expiring group memberships for the user.
///
/// # Parameters
Expand Down
7 changes: 7 additions & 0 deletions crates/core/src/identity/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ mock! {
user_id: &'a str,
) -> Result<(), IdentityProviderError>;

async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError>;

async fn get_group<'a>(
&self,
state: &ServiceState,
Expand Down
20 changes: 20 additions & 0 deletions crates/core/src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,26 @@ impl IdentityApi for IdentityProvider {
}
}

/// Update user.
///
/// # Parameters
/// - `state`: The service state.
/// - `user_id`: The ID of the user to update.
/// - `user`: The user details to update.
#[tracing::instrument(skip(self, state))]
async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError> {
match self {
Self::Service(provider) => provider.update_user(state, user_id, user).await,
#[cfg(any(test, feature = "mock"))]
Self::Mock(provider) => provider.update_user(state, user_id, user).await,
}
}

/// Get a service account by ID.
///
/// # Parameters
Expand Down
13 changes: 13 additions & 0 deletions crates/core/src/identity/provider_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,19 @@ pub trait IdentityApi: Send + Sync {
group_ids: HashSet<&'a str>,
) -> Result<(), IdentityProviderError>;

/// Update user.
///
/// # Parameters
/// - `state`: The service state.
/// - `user_id`: The ID of the user to update.
/// - `user`: The user details to update.
async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError>;

/// Set expiring group memberships of the user.
///
/// # Parameters
Expand Down
16 changes: 16 additions & 0 deletions crates/core/src/identity/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,22 @@ impl IdentityApi for IdentityService {
.await
}

/// Update user.
///
/// # Parameters
/// - `state`: The service state.
/// - `user_id`: The ID of the user to update.
/// - `user`: The user details to update.
async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError> {
user.validate()?;
self.backend_driver.update_user(state, user_id, user).await
}

/// Set expiring group memberships for the user.
///
/// # Parameters
Expand Down
1 change: 1 addition & 0 deletions crates/identity-driver-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ openstack-keystone-config.workspace = true
openstack-keystone-core.workspace = true
openstack-keystone-core-types.workspace = true
sea-orm.workspace = true
secrecy.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["macros"] }
Expand Down
30 changes: 25 additions & 5 deletions crates/identity-driver-sql/src/authenticate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ pub async fn authenticate_by_password(
return Err(AuthenticationError::UserLocked(local_user.user_id.clone()).into());
}

// Verify user exists
let user_entry = user::get_main_entry(db, &local_user.user_id).await?.ok_or(
IdentityProviderError::NoMainUserEntry(local_user.user_id.clone()),
)?;

// Check if the user is disabled
if !user_entry.enabled.unwrap_or(false) {
return Err(AuthenticationError::UserDisabled(local_user.user_id.clone()).into());
}

let passwords: Vec<db_password::Model> = password.into_iter().collect();
let latest_password = passwords
.first()
Expand Down Expand Up @@ -112,10 +122,6 @@ pub async fn authenticate_by_password(
}
}

let user_entry = user::get_main_entry(db, &local_user.user_id).await?.ok_or(
IdentityProviderError::NoMainUserEntry(local_user.user_id.clone()),
)?;

// Reset the last_active_at for the user that successfully authenticated.
user::reset_last_active(db, &user_entry).await?;

Expand Down Expand Up @@ -207,14 +213,15 @@ async fn should_lock(
#[cfg(test)]
mod tests {
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
use sea_orm::{DatabaseBackend, MockDatabase, Transaction};
use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction};
use tracing_test::traced_test;

use openstack_keystone_core_types::identity::UserOptions;

use super::*;
use crate::entity::local_user as db_local_user;
use crate::local_user::tests::get_local_user_mock;
use crate::user::tests::get_user_mock;

#[tokio::test]
async fn test_should_lock_default_config() {
Expand Down Expand Up @@ -439,7 +446,9 @@ mod tests {
"user_id",
&UserOptions::default(),
)])
// user::get_main_entry() for enabled check
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
// user update res for reset_last_active
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.into_connection();
assert!(
Expand Down Expand Up @@ -556,6 +565,10 @@ mod tests {
.build()
.unwrap(),
)]])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.append_query_results([user_option::tests::get_user_options_mock(
"user_id",
&UserOptions {
Expand All @@ -565,6 +578,10 @@ mod tests {
)])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.append_exec_results([MockExecResult {
rows_affected: 1,
..Default::default()
}])
.into_connection();
assert!(
authenticate_by_password(
Expand Down Expand Up @@ -598,6 +615,8 @@ mod tests {
"user_id",
&UserOptions::default(),
)])
// user::get_main_entry()
.append_query_results([vec![get_user_mock("user_id")]])
.into_connection();
match authenticate_by_password(
&config,
Expand Down Expand Up @@ -642,6 +661,7 @@ mod tests {
"user_id",
&UserOptions::default(),
)])
.append_query_results([vec![get_user_mock("user_id")]])
.into_connection();
match authenticate_by_password(
&config,
Expand Down
20 changes: 20 additions & 0 deletions crates/identity-driver-sql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,26 @@ impl IdentityBackend for SqlBackend {
)
.await?)
}

/// Update user.
///
/// # Parameters
/// - `state`: The service state.
/// - `user_id`: The ID of the user to update.
/// - `user`: The user details to update.
///
/// # Returns
/// A `Result` containing the updated `UserResponse` if successful, or an `Error`.
#[tracing::instrument(skip(self, state))]
async fn update_user<'a>(
&self,
state: &ServiceState,
user_id: &'a str,
user: UserUpdate,
) -> Result<UserResponse, IdentityProviderError> {
let config = state.config_manager.config.read().await;
Ok(user::update(&config, &state.db, user_id, user).await?)
}
}

#[async_trait]
Expand Down
Loading
Loading