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
179 changes: 111 additions & 68 deletions src/config.rs

Large diffs are not rendered by default.

14 changes: 0 additions & 14 deletions src/db/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,6 @@ impl Default for role::Model {
}
}

impl Default for user::Model {
fn default() -> Self {
Self {
id: String::new(),
extra: None,
enabled: None,
default_project_id: None,
created_at: None,
last_active_at: None,
domain_id: String::new(),
}
}
}

impl Default for service::Model {
fn default() -> Self {
Self {
Expand Down
1 change: 1 addition & 0 deletions src/db/entity/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use derive_builder::Builder;
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[cfg_attr(test, derive(Default))]
#[cfg_attr(test, derive(Builder))]
#[cfg_attr(test, builder(setter(strip_option, into)))]
#[sea_orm(table_name = "password")]
Expand Down
10 changes: 10 additions & 0 deletions src/db/entity/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,30 @@

//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.4

#[cfg(test)]
use derive_builder::Builder;
use serde::Deserialize;

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize)]
#[sea_orm(table_name = "user")]
#[cfg_attr(test, derive(Default))]
#[cfg_attr(test, derive(Builder))]
#[cfg_attr(test, builder(setter(strip_option, into)))]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(column_type = "Text", nullable)]
#[cfg_attr(test, builder(default))]
pub extra: Option<String>,
#[cfg_attr(test, builder(default))]
pub enabled: Option<bool>,
#[cfg_attr(test, builder(default))]
pub default_project_id: Option<String>,
#[cfg_attr(test, builder(default))]
pub created_at: Option<DateTime>,
#[cfg_attr(test, builder(default))]
pub last_active_at: Option<Date>,
pub domain_id: String,
}
Expand Down
3 changes: 3 additions & 0 deletions src/identity/backends/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub enum IdentityDatabaseError {
#[error("{0}")]
GroupNotFound(String),

#[error("Date calculation error")]
DateError,

#[error(transparent)]
Serde {
#[from]
Expand Down
91 changes: 66 additions & 25 deletions src/identity/backends/sql/authenticate.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
//! Authentication implementation.
//! User account authentication implementation.
use chrono::Utc;
use sea_orm::DatabaseConnection;
use tracing::info;
Expand Down Expand Up @@ -62,7 +62,7 @@ pub async fn authenticate_by_password(
if !user_opts
.ignore_lockout_failure_attempts
.is_some_and(|val| val)
&& is_account_locked(config, db, &local_user).await?
&& should_lock(config, db, &local_user).await?
{
return Err(AuthenticationError::UserLocked(local_user.user_id.clone()))?;
}
Expand Down Expand Up @@ -95,15 +95,26 @@ pub async fn authenticate_by_password(
}
}

let user = user::get_main_entry(db, &local_user.user_id).await?.ok_or(
let user_entry = user::get_main_entry(db, &local_user.user_id).await?.ok_or(
IdentityDatabaseError::NoMainUserEntry(local_user.user_id.clone()),
)?;
let user =
local_user::get_local_user_builder(config, &user, local_user, Some(passwords), user_opts)
.build()?;

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

let user_entry = UserResponseBuilder::default()
.merge_user_data(
&user_entry,
&user_opts,
config.get_user_last_activity_cutof_date().as_ref(),
)
.merge_local_user_data(&local_user)
.merge_passwords_data(passwords)
.build()?;

Ok(AuthenticatedInfo::builder()
.user_id(user.id.clone())
.user(user)
.user_id(user_entry.id.clone())
.user(user_entry)
.methods(vec!["password".into()])
.build()
.map_err(AuthenticationError::from)?)
Expand All @@ -116,7 +127,7 @@ pub async fn authenticate_by_password(
/// attempts as described by
/// [ADR-10](https://openstack-experimental.github.io/keystone/adr/0010-pci-dss-failed-auth-protection.html)
#[tracing::instrument(level = "debug", skip(config, db))]
async fn is_account_locked(
async fn should_lock(
config: &Config,
db: &DatabaseConnection,
local_user: &db_local_user::Model,
Expand Down Expand Up @@ -164,24 +175,24 @@ mod tests {
use super::*;

#[tokio::test]
async fn test_is_account_locked_default_config() {
async fn test_should_lock_default_config() {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
let config = Config::default();
assert!(
!is_account_locked(&config, &db, &db_local_user::Model::default(),)
!should_lock(&config, &db, &db_local_user::Model::default(),)
.await
.unwrap(),
"Default config does not request any validation and user is not considered locked"
);
}

#[tokio::test]
async fn test_is_account_locked_no_failed_auth_count() {
async fn test_should_lock_no_failed_auth_count() {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
let mut config = Config::default();
config.security_compliance.lockout_failure_attempts = Some(5);
assert!(
!is_account_locked(
!should_lock(
&config,
&db,
&db_local_user::Model {
Expand All @@ -195,7 +206,7 @@ mod tests {
"User with unset failed_auth props is not considered locked"
);
assert!(
!is_account_locked(
!should_lock(
&config,
&db,
&db_local_user::Model {
Expand All @@ -211,14 +222,14 @@ mod tests {
}

#[tokio::test]
async fn test_is_account_locked_no_failed_auth_at() {
async fn test_should_lock_no_failed_auth_at() {
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([vec![db_local_user::Model::default()]])
.into_connection();
let mut config = Config::default();
config.security_compliance.lockout_failure_attempts = Some(5);
assert!(
!is_account_locked(
!should_lock(
&config,
&db,
&db_local_user::Model {
Expand Down Expand Up @@ -248,15 +259,15 @@ mod tests {
}

#[tokio::test]
async fn test_is_account_locked_expired() {
async fn test_should_lock_expired() {
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([vec![db_local_user::Model::default()]])
.into_connection();
let mut config = Config::default();
config.security_compliance.lockout_failure_attempts = Some(5);
config.security_compliance.lockout_duration = Some(TimeDelta::seconds(100));
assert!(
!is_account_locked(
!should_lock(
&config,
&db,
&db_local_user::Model {
Expand Down Expand Up @@ -291,12 +302,12 @@ mod tests {
}

#[tokio::test]
async fn test_is_account_locked_lock() {
async fn test_should_lock_lock() {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
let mut config = Config::default();
config.security_compliance.lockout_failure_attempts = Some(5);
assert!(
is_account_locked(
should_lock(
&config,
&db,
&db_local_user::Model {
Expand All @@ -309,7 +320,7 @@ mod tests {
"User with failed_auth_count > lockout_failure_attempts is locked for lockout_duration",
);
assert!(
is_account_locked(
should_lock(
&config,
&db,
&db_local_user::Model {
Expand All @@ -322,7 +333,7 @@ mod tests {
"User with failed_auth_count = lockout_failure_attempts is locked for lockout_duration",
);
assert!(
!is_account_locked(
!should_lock(
&config,
&db,
&db_local_user::Model {
Expand Down Expand Up @@ -361,7 +372,8 @@ mod tests {
.append_query_results([user_option::tests::get_user_options_mock(
&UserOptions::default(),
)])
.append_query_results([vec![user::tests::get_user_mock("1")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.into_connection();
assert!(
authenticate_by_password(
Expand All @@ -377,6 +389,33 @@ mod tests {
.is_ok(),
"unlocked user with correct password should be allowed to login"
);

// Checking transaction log
assert_eq!(
db.into_transaction_log(),
[
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "local_user"."id" AS "A_id", "local_user"."user_id" AS "A_user_id", "local_user"."domain_id" AS "A_domain_id", "local_user"."name" AS "A_name", "local_user"."failed_auth_count" AS "A_failed_auth_count", "local_user"."failed_auth_at" AS "A_failed_auth_at", "password"."id" AS "B_id", "password"."local_user_id" AS "B_local_user_id", "password"."self_service" AS "B_self_service", "password"."created_at" AS "B_created_at", "password"."expires_at" AS "B_expires_at", "password"."password_hash" AS "B_password_hash", "password"."created_at_int" AS "B_created_at_int", "password"."expires_at_int" AS "B_expires_at_int" FROM "local_user" LEFT JOIN "password" ON "local_user"."id" = "password"."local_user_id" WHERE "local_user"."user_id" = $1 ORDER BY "local_user"."id" ASC, "password"."created_at_int" DESC"#,
["user_id".into()]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "user_option"."user_id", "user_option"."option_id", "user_option"."option_value" FROM "user_option" WHERE "user_option"."user_id" = $1"#,
["user_id".into()]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "user"."id", "user"."extra", "user"."enabled", "user"."default_project_id", "user"."created_at", "user"."last_active_at", "user"."domain_id" FROM "user" WHERE "user"."id" = $1 LIMIT $2"#,
["user_id".into(), 1u64.into()]
),
Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"UPDATE "user" SET "last_active_at" = $1 WHERE "user"."id" = $2 RETURNING "id", "extra", "enabled", "default_project_id", "created_at", "last_active_at", "domain_id""#,
[Utc::now().date_naive().into(), "user_id".into()]
),
]
);
}

#[tokio::test]
Expand Down Expand Up @@ -453,7 +492,8 @@ mod tests {
ignore_lockout_failure_attempts: Some(true),
..Default::default()
})])
.append_query_results([vec![user::tests::get_user_mock("1")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.into_connection();
assert!(
authenticate_by_password(
Expand Down Expand Up @@ -573,7 +613,8 @@ mod tests {
ignore_password_expiry: Some(true),
..Default::default()
})])
.append_query_results([vec![user::tests::get_user_mock("1")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.append_query_results([vec![user::tests::get_user_mock("user_id")]])
.into_connection();
assert!(
authenticate_by_password(
Expand Down
49 changes: 24 additions & 25 deletions src/identity/backends/sql/federated_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
//
// SPDX-License-Identifier: Apache-2.0

use super::user;
use crate::db::entity::{federated_user as db_federated_user, user as db_user};
use crate::db::entity::federated_user as db_federated_user;
use crate::identity::types::*;

mod create;
Expand All @@ -22,30 +21,30 @@ mod find;
pub use create::create;
pub use find::find_by_idp_and_unique_id;

pub fn get_federated_user_builder<F: IntoIterator<Item = db_federated_user::Model>>(
user: &db_user::Model,
data: F,
opts: UserOptions,
) -> UserResponseBuilder {
let mut user_builder: UserResponseBuilder = user::get_user_builder(user, opts);
let mut feds: Vec<Federation> = Vec::new();
if let Some(first) = data.into_iter().next() {
if let Some(name) = first.display_name {
user_builder.name(name.clone());
}
impl UserResponseBuilder {
pub fn merge_federated_user_data<I>(&mut self, data: I) -> &mut Self
where
I: IntoIterator<Item = db_federated_user::Model>,
{
let mut feds: Vec<Federation> = Vec::new();
if let Some(first) = data.into_iter().next() {
if let Some(name) = first.display_name {
self.name(name.clone());
}

let mut fed = FederationBuilder::default();
fed.idp_id(first.idp_id.clone());
fed.unique_id(first.unique_id.clone());
let protocol = FederationProtocol {
protocol_id: first.protocol_id.clone(),
unique_id: first.unique_id.clone(),
};
fed.protocols(vec![protocol]);
if let Ok(fed_obj) = fed.build() {
feds.push(fed_obj);
let mut fed = FederationBuilder::default();
fed.idp_id(first.idp_id.clone());
fed.unique_id(first.unique_id.clone());
let protocol = FederationProtocol {
protocol_id: first.protocol_id.clone(),
unique_id: first.unique_id.clone(),
};
fed.protocols(vec![protocol]);
if let Ok(fed_obj) = fed.build() {
feds.push(fed_obj);
}
}
self.federated(feds);
self
}
user_builder.federated(feds);
user_builder
}
Loading
Loading