diff --git a/src/api/v3/user/mod.rs b/src/api/v3/user/mod.rs index b8e5b27f..abbc4788 100644 --- a/src/api/v3/user/mod.rs +++ b/src/api/v3/user/mod.rs @@ -19,6 +19,7 @@ use axum::{ response::IntoResponse, }; use utoipa_axum::{router::OpenApiRouter, routes}; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -54,6 +55,7 @@ async fn list( Query(query): Query, State(state): State, ) -> Result { + query.validate()?; let users: Vec = state .provider .get_identity_provider() @@ -262,6 +264,7 @@ mod tests { UserListParameters { domain_id: Some("domain".into()), name: Some("name".into()), + ..Default::default() } == *qp }) .returning(|_, _| Ok(Vec::new())); diff --git a/src/api/v3/user/types.rs b/src/api/v3/user/types.rs index 02631260..15a4ded9 100644 --- a/src/api/v3/user/types.rs +++ b/src/api/v3/user/types.rs @@ -25,15 +25,16 @@ use validator::Validate; use crate::identity::types as identity_types; +/// User response object. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct User { - /// User ID + /// User ID. #[validate(length(max = 64))] pub id: String, - /// User domain ID + /// User domain ID. #[validate(length(max = 64))] pub domain_id: String, - /// User name + /// User name. #[validate(length(max = 255))] pub name: String, /// If the user is enabled, this value is true. If the user is disabled, @@ -64,8 +65,13 @@ pub struct User { #[serde(skip_serializing_if = "Option::is_none")] #[validate(nested)] pub options: Option, + /// List of federated objects associated with a user. Each object in the list contains the idp_id and protocols. protocols is a list of objects, each of which contains protocol_id and unique_id of the protocol and user respectively. + #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] + pub federated: Option>, } +/// Complete response with the user data. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserResponse { /// User object @@ -73,9 +79,10 @@ pub struct UserResponse { pub user: User, } +/// Create user data. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserCreate { - /// User domain ID + /// User domain ID. #[validate(length(max = 64))] pub domain_id: String, /// The user name. Must be unique within the owning domain. @@ -110,6 +117,7 @@ pub struct UserCreate { pub extra: Option, } +/// Update user data. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserUpdateRequest { /// The user name. Must be unique within the owning domain. @@ -143,6 +151,7 @@ pub struct UserUpdateRequest { pub extra: Option, } +/// User options. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserOptions { #[serde(skip_serializing_if = "Option::is_none")] @@ -189,9 +198,10 @@ impl From for identity_types::UserOptions { } } +/// Complete create user request. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserCreateRequest { - /// User object + /// User object. #[validate(nested)] pub user: UserCreate, } @@ -221,6 +231,9 @@ impl From for User { extra: value.extra, password_expires_at: value.password_expires_at, options: opts, + federated: value + .federated + .map(|val| val.into_iter().map(Into::into).collect()), } } } @@ -242,6 +255,45 @@ impl From for identity_types::UserCreate { } } +/// User federation data. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct Federation { + /// Identity provider ID. + pub idp_id: String, + /// Protocols. + #[validate(nested)] + pub protocols: Vec, +} + +/// Federation protocol data. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] +pub struct FederationProtocol { + /// Federation protocol ID + #[validate(length(max = 64))] + pub protocol_id: String, + // TODO: unique ID should potentially belong to the IDP and not to the protocol + /// Unique ID of the associated user + #[validate(length(max = 64))] + pub unique_id: String, +} + +impl From for Federation { + fn from(value: identity_types::Federation) -> Self { + Self { + idp_id: value.idp_id, + protocols: value.protocols.into_iter().map(Into::into).collect(), + } + } +} +impl From for FederationProtocol { + fn from(value: identity_types::FederationProtocol) -> Self { + Self { + protocol_id: value.protocol_id, + unique_id: value.unique_id, + } + } +} + impl IntoResponse for UserResponse { fn into_response(self) -> Response { (StatusCode::OK, Json(self)).into_response() @@ -281,14 +333,18 @@ impl IntoResponse for UserList { } } +/// User list parameters. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams, Validate)] pub struct UserListParameters { - /// Filter users by Domain ID + /// Filter users by Domain ID. #[validate(length(max = 64))] pub domain_id: Option, - /// Filter users by Name + /// Filter users by Name. #[validate(length(max = 255))] pub name: Option, + /// Filter users by the federated unique ID. + #[validate(length(max = 64))] + pub unique_id: Option, } impl From for identity_types::UserListParameters { @@ -296,6 +352,7 @@ impl From for identity_types::UserListParameters { Self { domain_id: value.domain_id, name: value.name, + unique_id: value.unique_id, // limit: value.limit, } } diff --git a/src/api/v4/user/mod.rs b/src/api/v4/user/mod.rs index f6048aed..07fe242d 100644 --- a/src/api/v4/user/mod.rs +++ b/src/api/v4/user/mod.rs @@ -264,6 +264,7 @@ mod tests { UserListParameters { domain_id: Some("domain".into()), name: Some("name".into()), + ..Default::default() } == *qp }) .returning(|_, _| Ok(Vec::new())); diff --git a/src/api/v4/user/types.rs b/src/api/v4/user/types.rs index 6cec4329..83f07ebb 100644 --- a/src/api/v4/user/types.rs +++ b/src/api/v4/user/types.rs @@ -11,263 +11,11 @@ // 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 serde_json::Value; -use utoipa::{IntoParams, ToSchema}; +//! User resource types. pub mod passkey; -use crate::identity::types as identity_types; - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct User { - /// User ID - pub id: String, - /// User domain ID - pub domain_id: String, - /// User name - pub name: String, - /// If the user is enabled, this value is true. If the user is disabled, - /// this value is false. - pub enabled: bool, - /// The ID of the default project for the user. A user’s default project - /// must not be a domain. Setting this attribute does not grant any - /// actual authorization on the project, and is merely provided for - /// convenience. Therefore, the referenced project does not need to exist - /// within the user domain. (Since v3.1) If the user does not have - /// authorization to their default project, the default project is - /// ignored at token creation. (Since v3.1) Additionally, if your - /// default project is not valid, a token is issued without an explicit - /// scope of authorization. - #[serde(skip_serializing_if = "Option::is_none")] - pub default_project_id: Option, - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub extra: Option, - /// The date and time when the password expires. The time zone is UTC. - #[serde(skip_serializing_if = "Option::is_none")] - pub password_expires_at: Option>, - /// The resource options for the user. Available resource options are - /// ignore_change_password_upon_first_use, ignore_password_expiry, - /// ignore_lockout_failure_attempts, lock_password, - /// multi_factor_auth_enabled, and multi_factor_auth_rules - /// ignore_user_inactivity. - #[serde(skip_serializing_if = "Option::is_none")] - pub options: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserResponse { - /// User object - pub user: User, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserCreate { - /// User domain ID - pub domain_id: String, - /// The user name. Must be unique within the owning domain. - pub name: String, - /// If the user is enabled, this value is true. If the user is disabled, - /// this value is false. - pub enabled: Option, - /// The ID of the default project for the user. A user’s default project - /// must not be a domain. Setting this attribute does not grant any - /// actual authorization on the project, and is merely provided for - /// convenience. Therefore, the referenced project does not need to exist - /// within the user domain. (Since v3.1) If the user does not have - /// authorization to their default project, the default project is - /// ignored at token creation. (Since v3.1) Additionally, if your - /// default project is not valid, a token is issued without an explicit - /// scope of authorization. - pub default_project_id: Option, - /// The password for the user. - pub password: Option, - /// The resource options for the user. Available resource options are - /// ignore_change_password_upon_first_use, ignore_password_expiry, - /// ignore_lockout_failure_attempts, lock_password, - /// multi_factor_auth_enabled, and multi_factor_auth_rules - /// ignore_user_inactivity. - pub options: Option, - /// Additional user properties - #[serde(flatten)] - pub extra: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserUpdateRequest { - /// The user name. Must be unique within the owning domain. - pub name: Option, - /// If the user is enabled, this value is true. If the user is disabled, - /// this value is false. - pub enabled: Option, - /// The ID of the default project for the user. A user’s default project - /// must not be a domain. Setting this attribute does not grant any - /// actual authorization on the project, and is merely provided for - /// convenience. Therefore, the referenced project does not need to exist - /// within the user domain. (Since v3.1) If the user does not have - /// authorization to their default project, the default project is - /// ignored at token creation. (Since v3.1) Additionally, if your - /// default project is not valid, a token is issued without an explicit - /// scope of authorization. - pub default_project_id: Option, - /// The password for the user. - pub password: Option, - /// The resource options for the user. Available resource options are - /// ignore_change_password_upon_first_use, ignore_password_expiry, - /// ignore_lockout_failure_attempts, lock_password, - /// multi_factor_auth_enabled, and multi_factor_auth_rules - /// ignore_user_inactivity. - pub options: Option, - /// Additional user properties - #[serde(flatten)] - pub extra: Option, -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserOptions { - #[serde(skip_serializing_if = "Option::is_none")] - pub ignore_change_password_upon_first_use: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ignore_password_expiry: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ignore_lockout_failure_attempts: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub lock_password: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ignore_user_inactivity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub multi_factor_auth_rules: Option>>, - #[serde(skip_serializing_if = "Option::is_none")] - pub multi_factor_auth_enabled: Option, -} - -impl From for UserOptions { - fn from(value: identity_types::UserOptions) -> Self { - Self { - ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, - ignore_password_expiry: value.ignore_password_expiry, - ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, - lock_password: value.lock_password, - ignore_user_inactivity: value.ignore_user_inactivity, - multi_factor_auth_rules: value.multi_factor_auth_rules, - multi_factor_auth_enabled: value.multi_factor_auth_enabled, - } - } -} - -impl From for identity_types::UserOptions { - fn from(value: UserOptions) -> Self { - Self { - ignore_change_password_upon_first_use: value.ignore_change_password_upon_first_use, - ignore_password_expiry: value.ignore_password_expiry, - ignore_lockout_failure_attempts: value.ignore_lockout_failure_attempts, - lock_password: value.lock_password, - ignore_user_inactivity: value.ignore_user_inactivity, - multi_factor_auth_rules: value.multi_factor_auth_rules, - multi_factor_auth_enabled: value.multi_factor_auth_enabled, - } - } -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserCreateRequest { - /// User object - pub user: UserCreate, -} - -impl From for User { - fn from(value: identity_types::UserResponse) -> Self { - let opts: UserOptions = value.options.clone().into(); - // We only want to see user options if there is at least 1 option set - let opts = if opts.ignore_change_password_upon_first_use.is_some() - || opts.ignore_password_expiry.is_some() - || opts.ignore_lockout_failure_attempts.is_some() - || opts.lock_password.is_some() - || opts.ignore_user_inactivity.is_some() - || opts.multi_factor_auth_rules.is_some() - || opts.multi_factor_auth_enabled.is_some() - { - Some(opts) - } else { - None - }; - Self { - id: value.id, - domain_id: value.domain_id, - name: value.name, - enabled: value.enabled, - default_project_id: value.default_project_id, - extra: value.extra, - password_expires_at: value.password_expires_at, - options: opts, - } - } -} - -impl From for identity_types::UserCreate { - fn from(value: UserCreateRequest) -> Self { - let user = value.user; - Self { - id: String::new(), - name: user.name, - domain_id: user.domain_id, - enabled: user.enabled, - password: user.password, - extra: user.extra, - default_project_id: user.default_project_id, - options: user.options.map(Into::into), - federated: None, - } - } -} - -impl IntoResponse for UserResponse { - fn into_response(self) -> Response { - (StatusCode::OK, Json(self)).into_response() - } -} - -/// Users -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct UserList { - /// Collection of user objects - pub users: Vec, -} - -impl From> for UserList { - fn from(value: Vec) -> Self { - let objects: Vec = value.into_iter().map(User::from).collect(); - Self { users: objects } - } -} - -impl IntoResponse for UserList { - fn into_response(self) -> Response { - (StatusCode::OK, Json(self)).into_response() - } -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams)] -pub struct UserListParameters { - /// Filter users by Domain ID - pub domain_id: Option, - /// Filter users by Name - pub name: Option, -} - -impl From for identity_types::UserListParameters { - fn from(value: UserListParameters) -> Self { - Self { - domain_id: value.domain_id, - name: value.name, - // limit: value.limit, - } - } -} +pub use crate::api::v3::user::types::{ + Federation, FederationProtocol, User, UserCreate, UserCreateRequest, UserList, + UserListParameters, UserOptions, UserResponse, UserUpdateRequest, +}; diff --git a/src/identity/types/user.rs b/src/identity/types/user.rs index 4091e9a2..c9dcc4c5 100644 --- a/src/identity/types/user.rs +++ b/src/identity/types/user.rs @@ -16,28 +16,34 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use serde_json::Value; +use validator::Validate; mod webauthn_credential; pub use webauthn_credential::WebauthnCredential; -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct UserResponse { /// The user ID. + #[validate(length(max = 64))] pub id: String, /// The user name. Must be unique within the owning domain. + #[validate(length(max = 255))] pub name: String, /// The ID of the domain. + #[validate(length(max = 64))] pub domain_id: String, /// If the user is enabled, this value is true. If the user is disabled, /// this value is false. pub enabled: bool, /// The resource description #[builder(default)] + #[validate(length(max = 255))] pub description: Option, /// The ID of the default project for the user. #[builder(default)] + #[validate(length(max = 64))] pub default_project_id: Option, /// Additional user properties #[builder(default)] @@ -47,50 +53,60 @@ pub struct UserResponse { pub password_expires_at: Option>, /// The resource options for the user. #[builder(default)] + #[validate(nested)] pub options: UserOptions, /// List of federated objects associated with a user. Each object in the /// list contains the idp_id and protocols. protocols is a list of objects, /// each of which contains protocol_id and unique_id of the protocol and /// user respectively. #[builder(default)] + #[validate(nested)] pub federated: Option>, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct UserCreate { + #[validate(length(max = 64))] pub id: String, /// The user name. Must be unique within the owning domain. + #[validate(length(max = 255))] pub name: String, /// The ID of the domain. + #[validate(length(max = 64))] pub domain_id: String, /// If the user is enabled, this value is true. If the user is disabled, /// this value is false. pub enabled: Option, /// The ID of the default project for the user. #[builder(default)] + #[validate(length(max = 64))] pub default_project_id: Option, /// User password #[builder(default)] + #[validate(length(max = 72))] pub password: Option, /// Additional user properties #[builder(default)] pub extra: Option, /// The resource options for the user. #[builder(default)] + #[validate(nested)] pub options: Option, /// List of federated objects associated with a user. Each object in the /// list contains the idp_id and protocols. protocols is a list of objects, /// each of which contains protocol_id and unique_id of the protocol and /// user respectively. #[builder(default)] + #[validate(nested)] pub federated: Option>, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(into))] pub struct UserUpdate { /// The user name. Must be unique within the owning domain. + #[validate(length(max = 64))] #[builder(default)] pub name: Option>, /// If the user is enabled, this value is true. If the user is disabled, @@ -99,28 +115,33 @@ pub struct UserUpdate { pub enabled: Option, /// The resource description #[builder(default)] + #[validate(length(max = 255))] pub description: Option>, /// The ID of the default project for the user. #[builder(default)] + #[validate(length(max = 64))] pub default_project_id: Option>, /// User password #[builder(default)] + #[validate(length(max = 72))] pub password: Option, /// Additional user properties #[builder(default)] pub extra: Option, /// The resource options for the user. #[builder(default)] + #[validate(nested)] pub options: Option, /// List of federated objects associated with a user. Each object in the /// list contains the idp_id and protocols. protocols is a list of objects, /// each of which contains protocol_id and unique_id of the protocol and /// user respectively. #[builder(default)] + #[validate(nested)] pub federated: Option>, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct UserOptions { pub ignore_change_password_upon_first_use: Option, @@ -132,63 +153,85 @@ pub struct UserOptions { pub multi_factor_auth_enabled: Option, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +/// User federation data. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct Federation { - /// Identity provider ID + /// Identity provider ID. + #[validate(length(max = 64))] pub idp_id: String, - /// Protocols + /// Protocols. #[builder(default)] + #[validate(nested)] pub protocols: Vec, + /// Unique ID of the user within the IdP. #[builder] pub unique_id: String, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +/// Federation protocol data. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct FederationProtocol { - /// Federation protocol ID + /// Federation protocol ID. + #[validate(length(max = 64))] pub protocol_id: String, // TODO: unique ID should potentially belong to the IDP and not to the protocol - /// Unique ID of the associated user + /// Unique ID of the associated user. + #[validate(length(max = 64))] pub unique_id: String, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +/// User listing parameters. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] pub struct UserListParameters { - /// Filter users by the domain + /// Filter users by the domain. + #[builder(default)] + #[validate(length(max = 64))] pub domain_id: Option, - /// Filter users by the name attribute + /// Filter users by the name attribute. + #[builder(default)] + #[validate(length(max = 255))] pub name: Option, + /// Filter users by the federated unique ID. + #[builder(default)] + #[validate(length(max = 64))] + pub unique_id: Option, } -/// User password information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +/// User password information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct UserPasswordAuthRequest { - /// User ID + /// User ID. #[builder(default)] + #[validate(length(max = 64))] pub id: Option, - /// User Name + /// User Name. #[builder(default)] + #[validate(length(max = 255))] pub name: Option, - /// User domain + /// User domain. #[builder(default)] + #[validate(nested)] pub domain: Option, - /// User password expiry date + /// User password expiry date. #[builder(default)] + #[validate(length(max = 72))] pub password: String, } -/// Domain information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +/// Domain information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, Validate)] #[builder(setter(strip_option, into))] pub struct Domain { /// Domain ID #[builder(default)] + #[validate(length(max = 64))] pub id: Option, /// Domain Name #[builder(default)] + #[validate(length(max = 255))] pub name: Option, }