From 833daa060e197195d9006aabdc906c7ec4f9edbf Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 2 Dec 2025 15:40:11 +0100 Subject: [PATCH] feat: Add request validation Maybe not complete in the coverage, but adds request schema validation based on the `validator` crate. `utoipa` and `schemars` are describing the schema, but not enforcing, thus need to add something else. Fixes: #324 --- Cargo.lock | 31 ++ Cargo.toml | 1 + src/api/error.rs | 8 + src/api/types.rs | 63 ++- src/api/v3/auth/token/create.rs | 2 + src/api/v3/auth/token/types.rs | 55 ++- src/api/v3/group/types.rs | 25 +- src/api/v3/role/types.rs | 17 +- src/api/v3/role_assignment/types.rs | 46 ++- src/api/v3/user/types.rs | 37 +- src/api/v4/auth/passkey/finish.rs | 2 + src/api/v4/auth/passkey/start.rs | 2 + src/api/v4/auth/passkey/types.rs | 34 +- src/api/v4/auth/token/types.rs | 51 ++- src/api/v4/federation/auth.rs | 2 + .../v4/federation/identity_provider/create.rs | 2 + .../v4/federation/identity_provider/update.rs | 2 + src/api/v4/federation/mapping/create.rs | 2 + src/api/v4/federation/mapping/update.rs | 2 + src/api/v4/federation/types/auth.rs | 12 +- .../v4/federation/types/identity_provider.rs | 44 +- src/api/v4/federation/types/mapping.rs | 43 +- src/api/v4/token/restriction/create.rs | 2 + src/api/v4/token/restriction/update.rs | 2 + src/api/v4/token/types/restriction.rs | 32 +- src/api/v4/user/passkey/register_finish.rs | 2 + src/api/v4/user/passkey/register_start.rs | 2 + src/api/v4/user/types/passkey.rs | 55 ++- src/token/mod.rs | 385 ++++++++++-------- 29 files changed, 655 insertions(+), 308 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5d365aa..80804e74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,6 +2896,7 @@ dependencies = [ "utoipa-axum", "utoipa-swagger-ui", "uuid", + "validator", "webauthn-rs", "webauthn-rs-proto", ] @@ -5378,6 +5379,36 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" +dependencies = [ + "darling 0.20.11", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 553d1c16..3f4ef63f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ utoipa = { version = "5.4", features = ["axum_extras", "chrono", "yaml"] } utoipa-axum = { version = "0.2" } utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"], default-features = false } uuid = { version = "1.18", features = ["v4"] } +validator = { version = "0.20", features = ["derive"] } webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation"] } webauthn-rs-proto = { version = "0.5" } diff --git a/src/api/error.rs b/src/api/error.rs index ff973e44..6e419f51 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -170,6 +170,14 @@ pub enum KeystoneApiError { #[error("changing current authentication scope is forbidden")] AuthenticationRescopeForbidden, + /// Request validation error. + #[error("request validation failed: {source}")] + Validator { + /// The source of the error. + #[from] + source: validator::ValidationErrors, + }, + /// Others. #[error(transparent)] Other(#[from] eyre::Report), diff --git a/src/api/types.rs b/src/api/types.rs index 424b722c..e37fe0c8 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -21,12 +21,14 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use validator::{Validate, ValidationErrors}; use crate::catalog::types::{Endpoint as ProviderEndpoint, Service}; use crate::resource::types as resource_provider_types; -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Versions { + #[validate(nested)] pub versions: Values, } @@ -36,13 +38,15 @@ impl IntoResponse for Versions { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Values { + #[validate(nested)] pub values: Vec, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct SingleVersion { + #[validate(nested)] pub version: Version, } @@ -52,15 +56,18 @@ impl IntoResponse for SingleVersion { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Version { + #[validate(length(max = 5))] pub id: String, pub status: VersionStatus, #[serde(skip_serializing_if = "Option::is_none")] pub updated: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub links: Option>, #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub media_types: Option>, } @@ -73,9 +80,11 @@ pub enum VersionStatus { Experimental, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Link { + #[validate(length(max = 10))] pub rel: String, + #[validate(url)] pub href: String, } @@ -88,7 +97,7 @@ impl Link { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct MediaType { pub base: String, pub r#type: String, @@ -107,6 +116,12 @@ impl Default for MediaType { #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] pub struct Catalog(Vec); +impl Validate for Catalog { + fn validate(&self) -> Result<(), ValidationErrors> { + self.0.validate() + } +} + impl IntoResponse for Catalog { fn into_response(self) -> Response { (StatusCode::OK, Json(self)).into_response() @@ -114,12 +129,15 @@ impl IntoResponse for Catalog { } /// A catalog object. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct CatalogService { pub r#type: Option, + #[validate(length(max = 255))] pub name: Option, + #[validate(length(max = 64))] pub id: String, + #[validate(nested)] pub endpoints: Vec, } @@ -135,15 +153,20 @@ impl From<(Service, Vec)> for CatalogService { } /// A Catalog Endpoint. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct Endpoint { + #[validate(length(max = 64))] pub id: String, + #[validate(url)] pub url: String, + #[validate(length(max = 64))] pub interface: String, #[builder(default)] + #[validate(length(max = 64))] pub region: Option, #[builder(default)] + #[validate(length(max = 64))] pub region_id: Option, } @@ -190,15 +213,27 @@ pub enum Scope { System(System), } +impl Validate for Scope { + fn validate(&self) -> Result<(), ValidationErrors> { + match self { + Self::Project(project) => project.validate(), + Self::Domain(domain) => domain.validate(), + Self::System(system) => system.validate(), + } + } +} + /// Project scope information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct ProjectScope { /// Project ID. #[builder(default)] + #[validate(length(max = 64))] pub id: Option, /// Project Name. #[builder(default)] + #[validate(length(max = 64))] pub name: Option, /// Project domain. #[builder(default)] @@ -206,30 +241,34 @@ pub struct ProjectScope { } /// Domain information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct Domain { /// Domain ID. #[builder(default)] + #[validate(length(max = 64))] pub id: Option, /// Domain Name. #[builder(default)] + #[validate(length(max = 64))] pub name: Option, } /// Project information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Project { /// Project ID. + #[validate(length(max = 64))] pub id: String, /// Project Name. + #[validate(length(max = 64))] pub name: String, /// project domain. pub domain: Domain, } /// System scope. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct System { /// All systems access. diff --git a/src/api/v3/auth/token/create.rs b/src/api/v3/auth/token/create.rs index 3a06c252..940625c6 100644 --- a/src/api/v3/auth/token/create.rs +++ b/src/api/v3/auth/token/create.rs @@ -19,6 +19,7 @@ use axum::{ http::StatusCode, response::IntoResponse, }; +use validator::Validate; use crate::api::v3::auth::token::common::{authenticate_request, get_authz_info}; use crate::api::v3::auth::token::types::{ @@ -46,6 +47,7 @@ pub(super) async fn create( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; let authed_info = authenticate_request(&state, &req).await?; let authz_info = get_authz_info(&state, &req).await?; if let Some(restriction_id) = &authed_info.token_restriction_id { diff --git a/src/api/v3/auth/token/types.rs b/src/api/v3/auth/token/types.rs index 4b1804d7..193b89b0 100644 --- a/src/api/v3/auth/token/types.rs +++ b/src/api/v3/auth/token/types.rs @@ -21,6 +21,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::api::error::TokenError; use crate::api::types::*; @@ -29,7 +30,7 @@ use crate::identity::types as identity_types; use crate::token::Token as BackendToken; /// Authorization token -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct Token { /// A list of one or two audit IDs. An audit ID is a unique, randomly @@ -61,6 +62,7 @@ pub struct Token { /// A user object. //#[builder(default)] + #[validate(nested)] pub user: User, /// A project object including the id, name and domain object representing @@ -68,6 +70,7 @@ pub struct Token { /// that are scoped to a project. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub project: Option, /// A domain object including the id and name representing the domain the @@ -75,23 +78,27 @@ pub struct Token { /// to a domain. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub domain: Option, /// A list of role objects #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub roles: Option>, /// A catalog object. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub catalog: Option, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenResponse { /// Token + #[validate(nested)] pub token: Token, } @@ -102,16 +109,18 @@ impl IntoResponse for TokenResponse { } /// An authentication request. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthRequest { /// An identity object. + #[validate(nested)] pub auth: AuthRequestInner, } /// An authentication request. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthRequestInner { /// An identity object. + #[validate(nested)] pub identity: Identity, /// The authorization scope, including the system (Since v3.10), a project, @@ -124,11 +133,12 @@ pub struct AuthRequestInner { /// specified in order to uniquely identify the project by name. A domain /// scope may be specified by either the domain’s ID or name with /// equivalent results. + #[validate(nested)] pub scope: Option, } /// An identity object. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct Identity { /// The authentication method. For password authentication, specify @@ -137,36 +147,43 @@ pub struct Identity { /// The password object, contains the authentication information. #[builder(default)] + #[validate(nested)] pub password: Option, /// The token object, contains the authentication information. #[builder(default)] + #[validate(nested)] pub token: Option, } /// The password object, contains the authentication information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct PasswordAuth { /// A user object. #[builder(default)] + #[validate(nested)] pub user: UserPassword, } -/// User password information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +/// User password information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct UserPassword { - /// 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. + #[validate(length(max = 255))] pub password: String, } @@ -196,16 +213,19 @@ impl TryFrom for identity_types::UserPasswordAuthRequest { } } -/// User information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +/// User information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into, strip_option))] pub struct User { /// User ID + #[validate(length(max = 64))] pub id: String, /// User Name #[builder(default)] + #[validate(length(max = 255))] pub name: Option, /// User domain + #[validate(nested)] pub domain: Domain, /// User password expiry date #[builder(default)] @@ -226,21 +246,22 @@ impl TryFrom<&BackendToken> for Token { } /// The token object, contains the authentication information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenAuth { /// An authentication token. + #[validate(length(max = 1024))] pub id: String, } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct CreateTokenParameters { /// The authentication response excludes the service catalog. By default, /// the response includes the service catalog. pub nocatalog: Option, } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct ValidateTokenParameters { /// The authentication response excludes the service catalog. By default, /// the response includes the service catalog. diff --git a/src/api/v3/group/types.rs b/src/api/v3/group/types.rs index 391ddb1a..a49e045a 100644 --- a/src/api/v3/group/types.rs +++ b/src/api/v3/group/types.rs @@ -20,44 +20,54 @@ use axum::{ use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::identity::types; -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Group { /// Group ID + #[validate(length(max = 64))] pub id: String, /// Group domain ID + #[validate(length(max = 64))] pub domain_id: String, /// Group name + #[validate(length(max = 64))] pub name: String, /// Group description + #[validate(length(max = 255))] pub description: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] pub extra: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct GroupResponse { /// group object + #[validate(nested)] pub group: Group, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct GroupCreate { /// Group domain ID + #[validate(length(max = 64))] pub domain_id: String, /// Group name + #[validate(length(max = 64))] pub name: String, /// Group description + #[validate(length(max = 255))] pub description: Option, #[serde(default, flatten, skip_serializing_if = "Option::is_none")] pub extra: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct GroupCreateRequest { /// Group object + #[validate(nested)] pub group: GroupCreate, } @@ -105,9 +115,10 @@ impl IntoResponse for types::Group { } /// Groups -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct GroupList { /// Collection of group objects + #[validate(nested)] pub groups: Vec, } @@ -124,11 +135,13 @@ impl IntoResponse for GroupList { } } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct GroupListParameters { /// Filter users by Domain ID + #[validate(length(max = 64))] pub domain_id: Option, /// Filter users by Name + #[validate(length(max = 64))] pub name: Option, } diff --git a/src/api/v3/role/types.rs b/src/api/v3/role/types.rs index 073f5df5..b8525be0 100644 --- a/src/api/v3/role/types.rs +++ b/src/api/v3/role/types.rs @@ -20,28 +20,34 @@ use axum::{ use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::assignment::types; -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Role { /// Role ID + #[validate(length(max = 64))] pub id: String, /// Role domain ID #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub domain_id: Option, /// Role name + #[validate(length(max = 255))] pub name: String, /// Role description #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 255))] pub description: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] pub extra: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RoleResponse { /// Role object + #[validate(nested)] pub role: Role, } @@ -76,9 +82,10 @@ impl IntoResponse for types::Role { } /// Roles -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RoleList { /// Collection of role objects + #[validate(nested)] pub roles: Vec, } @@ -95,11 +102,13 @@ impl IntoResponse for RoleList { } } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct RoleListParameters { /// Filter users by Domain ID + #[validate(length(max = 64))] pub domain_id: Option, /// Filter users by Name + #[validate(length(max = 255))] pub name: Option, } diff --git a/src/api/v3/role_assignment/types.rs b/src/api/v3/role_assignment/types.rs index 567f88aa..0a6f0c1c 100644 --- a/src/api/v3/role_assignment/types.rs +++ b/src/api/v3/role_assignment/types.rs @@ -20,53 +20,65 @@ use axum::{ use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use validator::{Validate, ValidationErrors}; use crate::api::error::KeystoneApiError; use crate::assignment::types; -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct Assignment { /// Role ID + #[validate(nested)] pub role: Role, #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub user: Option, #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub group: Option, + #[validate(nested)] pub scope: Scope, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Role { + #[validate(length(max = 64))] pub id: String, #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub name: Option, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct User { + #[validate(length(max = 64))] pub id: String, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Group { + #[validate(length(max = 64))] pub id: String, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Project { + #[validate(length(max = 64))] pub id: String, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Domain { + #[validate(length(max = 64))] pub id: String, } -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct System { + #[validate(length(max = 64))] pub id: String, } @@ -78,6 +90,16 @@ pub enum Scope { System(System), } +impl Validate for Scope { + fn validate(&self) -> Result<(), ValidationErrors> { + match self { + Self::Project(project) => project.validate(), + Self::Domain(domain) => domain.validate(), + Self::System(system) => system.validate(), + } + } +} + impl TryFrom for Assignment { type Error = KeystoneApiError; @@ -154,9 +176,10 @@ impl From for KeystoneApiError } /// Assignments -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AssignmentList { /// Collection of role assignment objects + #[validate(nested)] pub role_assignments: Vec, } @@ -167,14 +190,16 @@ impl IntoResponse for AssignmentList { } /// List role assignments query parameters -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct RoleAssignmentListParameters { /// Filters the response by a domain ID. #[serde(rename = "scope.domain.id")] + #[validate(length(max = 64))] pub domain_id: Option, /// Filters the response by a group ID. #[serde(rename = "group.id")] + #[validate(length(max = 64))] pub group_id: Option, /// Returns the effective assignments, including any assignments gained by @@ -183,14 +208,17 @@ pub struct RoleAssignmentListParameters { /// Filters the response by a project ID. #[serde(rename = "scope.project.id")] + #[validate(length(max = 64))] pub project_id: Option, /// Filters the response by a role ID. #[serde(rename = "role.id")] + #[validate(length(max = 64))] pub role_id: Option, /// Filters the response by a user ID. #[serde(rename = "user.id")] + #[validate(length(max = 64))] pub user_id: Option, /// If set to true, then the names of any entities returned will be include diff --git a/src/api/v3/user/types.rs b/src/api/v3/user/types.rs index 65258c44..02631260 100644 --- a/src/api/v3/user/types.rs +++ b/src/api/v3/user/types.rs @@ -21,16 +21,20 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::identity::types as identity_types; -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct User { /// User ID + #[validate(length(max = 64))] pub id: String, /// User domain ID + #[validate(length(max = 64))] pub domain_id: String, /// User name + #[validate(length(max = 255))] pub name: String, /// If the user is enabled, this value is true. If the user is disabled, /// this value is false. @@ -45,6 +49,7 @@ pub struct User { /// default project is not valid, a token is issued without an explicit /// scope of authorization. #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub default_project_id: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] pub extra: Option, @@ -57,20 +62,24 @@ pub struct User { /// multi_factor_auth_enabled, and multi_factor_auth_rules /// ignore_user_inactivity. #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub options: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserResponse { /// User object + #[validate(nested)] pub user: User, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserCreate { /// User domain ID + #[validate(length(max = 64))] pub domain_id: String, /// The user name. Must be unique within the owning domain. + #[validate(length(max = 255))] pub name: String, /// If the user is enabled, this value is true. If the user is disabled, /// this value is false. @@ -84,23 +93,27 @@ pub struct UserCreate { /// 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. + #[validate(length(max = 64))] pub default_project_id: Option, /// The password for the user. + #[validate(length(max = 72))] 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. + #[validate(nested)] pub options: Option, /// Additional user properties #[serde(flatten)] pub extra: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserUpdateRequest { /// The user name. Must be unique within the owning domain. + #[validate(length(max = 255))] pub name: Option, /// If the user is enabled, this value is true. If the user is disabled, /// this value is false. @@ -114,6 +127,7 @@ pub struct UserUpdateRequest { /// 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. + #[validate(length(max = 64))] pub default_project_id: Option, /// The password for the user. pub password: Option, @@ -122,13 +136,14 @@ pub struct UserUpdateRequest { /// ignore_lockout_failure_attempts, lock_password, /// multi_factor_auth_enabled, and multi_factor_auth_rules /// ignore_user_inactivity. + #[validate(nested)] pub options: Option, /// Additional user properties #[serde(flatten)] pub extra: Option, } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserOptions { #[serde(skip_serializing_if = "Option::is_none")] pub ignore_change_password_upon_first_use: Option, @@ -174,9 +189,10 @@ impl From for identity_types::UserOptions { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserCreateRequest { /// User object + #[validate(nested)] pub user: UserCreate, } @@ -244,10 +260,11 @@ impl IntoResponse for identity_types::UserResponse { } } -/// Users -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +/// List of users. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserList { /// Collection of user objects + #[validate(nested)] pub users: Vec, } @@ -264,11 +281,13 @@ impl IntoResponse for UserList { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams, Validate)] pub struct UserListParameters { /// Filter users by Domain ID + #[validate(length(max = 64))] pub domain_id: Option, /// Filter users by Name + #[validate(length(max = 255))] pub name: Option, } diff --git a/src/api/v4/auth/passkey/finish.rs b/src/api/v4/auth/passkey/finish.rs index 2c72f594..78bdfcf3 100644 --- a/src/api/v4/auth/passkey/finish.rs +++ b/src/api/v4/auth/passkey/finish.rs @@ -15,6 +15,7 @@ use axum::{Json, extract::State, http::StatusCode, response::IntoResponse}; use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use tracing::debug; +use validator::Validate; use crate::api::v4::auth::passkey::types::{ AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, HmacGetSecretOutput, @@ -56,6 +57,7 @@ pub(super) async fn finish( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; let user_id = req.user_id.clone(); // TODO: Wrap all errors into the Unauthorized, but log the error if let Some(s) = state diff --git a/src/api/v4/auth/passkey/start.rs b/src/api/v4/auth/passkey/start.rs index fd7fc58c..595dc67f 100644 --- a/src/api/v4/auth/passkey/start.rs +++ b/src/api/v4/auth/passkey/start.rs @@ -15,6 +15,7 @@ use axum::{Json, extract::State, response::IntoResponse}; use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use tracing::debug; +use validator::Validate; use webauthn_rs::prelude::*; use super::types::*; @@ -46,6 +47,7 @@ pub(super) async fn start( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; // TODO: Check user existence and simulate the response when the user does not // exist. state diff --git a/src/api/v4/auth/passkey/types.rs b/src/api/v4/auth/passkey/types.rs index 9874376d..63c36e6e 100644 --- a/src/api/v4/auth/passkey/types.rs +++ b/src/api/v4/auth/passkey/types.rs @@ -16,16 +16,18 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use validator::Validate; /// Request for initialization of the passkey authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyAuthenticationStartRequest { /// The user authentication data + #[validate(nested)] pub passkey: PasskeyUserAuthenticationRequest, } /// Request for initialization of the passkey authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyUserAuthenticationRequest { /// The ID of the user that is authenticating. pub user_id: String, @@ -37,9 +39,10 @@ pub struct PasskeyUserAuthenticationRequest { /// handling. This is meant to be opaque, that is, you should not need to /// inspect or alter the content of the struct /// - you should serialise it and transmit it to the client only. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyAuthenticationStartResponse { /// The options. + #[validate(nested)] pub public_key: PublicKeyCredentialRequestOptions, /// The mediation requested. #[schema(nullable = false)] @@ -48,9 +51,10 @@ pub struct PasskeyAuthenticationStartResponse { } /// The requested options for the authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PublicKeyCredentialRequestOptions { /// The set of credentials that are allowed to sign this challenge. + #[validate(nested)] pub allow_credentials: Vec, /// The challenge that should be signed by the authenticator. #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -58,12 +62,14 @@ pub struct PublicKeyCredentialRequestOptions { /// extensions. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub extensions: Option, /// Hints defining which types credentials may be used in this operation. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] pub hints: Option>, /// The relying party ID. + #[validate(length(max = 64))] pub rp_id: String, /// The timeout for the authenticator in case of no interaction. pub timeout: Option, @@ -82,7 +88,7 @@ pub enum Mediation { } /// A descriptor of a credential that can be used. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AllowCredentials { /// The id of the credential. #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -189,7 +195,7 @@ pub enum PublicKeyCredentialHint { /// Extension option inputs for PublicKeyCredentialRequestOptions /// /// Implements AuthenticatorExtensionsClientInputs from the spec -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RequestAuthenticationExtensions { /// The appid extension options. #[schema(nullable = false)] @@ -199,6 +205,7 @@ pub struct RequestAuthenticationExtensions { /// Hmac get secret. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub hmac_get_secret: Option, /// ⚠️ - Browsers do not support this! Uvm. #[schema(nullable = false)] @@ -209,7 +216,7 @@ pub struct RequestAuthenticationExtensions { /// The inputs to the hmac secret if it was created during registration. /// /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct HmacGetSecretInput { /// Retrieve a symmetric secrets from the authenticator with this input. #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -226,25 +233,28 @@ pub struct HmacGetSecretInput { /// /// You should not need to handle the inner content of this structure - you /// should provide this to the correctly handling function of Webauthn only. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyAuthenticationFinishRequest { /// The credential Id, likely base64. pub id: String, /// Unsigned Client processed extensions. + #[validate(nested)] pub extensions: AuthenticationExtensionsClientOutputs, /// The binary of the credential id. #[schema(value_type = String, format = Binary, content_encoding = "base64")] pub raw_id: String, /// The authenticator response. + #[validate(nested)] pub response: AuthenticatorAssertionResponseRaw, /// The authenticator type. pub type_: String, /// The ID of the user. + #[validate(length(max = 64))] pub user_id: String, } /// [AuthenticatorAssertionResponseRaw](https://w3c.github.io/webauthn/#authenticatorassertionresponse) -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthenticatorAssertionResponseRaw { /// Raw authenticator data. #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -263,7 +273,7 @@ pub struct AuthenticatorAssertionResponseRaw { /// [AuthenticationExtensionsClientOutputs](https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs) /// /// The default option here for Options are None, so it can be derived -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthenticationExtensionsClientOutputs { /// Indicates whether the client used the provided appid extension. #[serde(skip_serializing_if = "Option::is_none")] @@ -272,11 +282,12 @@ pub struct AuthenticationExtensionsClientOutputs { /// The response to a hmac get secret request. #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(nested, required)] pub hmac_get_secret: Option, } /// The response to a hmac get secret request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct HmacGetSecretOutput { /// Output of HMAC(Salt 1 || Client Secret). #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -284,5 +295,6 @@ pub struct HmacGetSecretOutput { /// Output of HMAC(Salt 2 || Client Secret). #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false, value_type = String, format = Binary, content_encoding = "base64")] + #[validate(required)] pub output2: Option, } diff --git a/src/api/v4/auth/token/types.rs b/src/api/v4/auth/token/types.rs index 1610e3d9..4a66e11a 100644 --- a/src/api/v4/auth/token/types.rs +++ b/src/api/v4/auth/token/types.rs @@ -21,6 +21,7 @@ use chrono::{DateTime, Utc}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::api::error::TokenError; use crate::api::types::*; @@ -29,7 +30,7 @@ use crate::identity::types as identity_types; use crate::token::Token as BackendToken; /// Authorization token -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct Token { /// A list of one or two audit IDs. An audit ID is a unique, randomly @@ -61,6 +62,7 @@ pub struct Token { /// A user object. //#[builder(default)] + #[validate(nested)] pub user: User, /// A project object including the id, name and domain object representing @@ -68,6 +70,7 @@ pub struct Token { /// that are scoped to a project. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub project: Option, /// A domain object including the id and name representing the domain the @@ -75,23 +78,27 @@ pub struct Token { /// to a domain. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub domain: Option, /// A list of role objects #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub roles: Option>, /// A catalog object. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(nested)] pub catalog: Option, } -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenResponse { /// Token + #[validate(nested)] pub token: Token, } @@ -102,16 +109,18 @@ impl IntoResponse for TokenResponse { } /// An authentication request. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthRequest { /// An identity object. + #[validate(nested)] pub auth: AuthRequestInner, } /// An authentication request. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthRequestInner { /// An identity object. + #[validate(nested)] pub identity: Identity, /// The authorization scope, including the system (Since v3.10), a project, @@ -124,42 +133,50 @@ pub struct AuthRequestInner { /// specified in order to uniquely identify the project by name. A domain /// scope may be specified by either the domain’s ID or name with /// equivalent results. + #[validate(nested)] pub scope: Option, } /// An identity object. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Identity { /// The authentication method. For password authentication, specify /// password. pub methods: Vec, /// The password object, contains the authentication information. + #[validate(nested)] pub password: Option, /// The token object, contains the authentication information. + #[validate(nested)] pub token: Option, } /// The password object, contains the authentication information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct PasswordAuth { /// A user object. #[builder(default)] + #[validate(nested)] pub user: UserPassword, } /// User password information -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserPassword { - /// User ID + /// User ID. + #[validate(length(max = 64))] pub id: Option, - /// User Name + /// User Name. + #[validate(length(max = 64))] pub name: Option, - /// User domain + /// User domain. + #[validate(nested)] pub domain: Option, - /// User password expiry date + /// User password. + #[validate(length(max = 72))] pub password: String, } @@ -190,15 +207,18 @@ impl TryFrom for identity_types::UserPasswordAuthRequest { } /// User information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into))] pub struct User { /// User ID + #[validate(length(max = 64))] pub id: String, /// User Name #[builder(default)] + #[validate(length(max = 64))] pub name: Option, /// User domain + #[validate(nested)] pub domain: Domain, /// User password expiry date #[builder(default)] @@ -219,21 +239,22 @@ impl TryFrom<&BackendToken> for Token { } /// The token object, contains the authentication information. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenAuth { /// An authentication token. + #[validate(length(max = 1024))] pub id: String, } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct CreateTokenParameters { /// The authentication response excludes the service catalog. By default, /// the response includes the service catalog. pub nocatalog: Option, } -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct ValidateTokenParameters { /// The authentication response excludes the service catalog. By default, /// the response includes the service catalog. diff --git a/src/api/v4/federation/auth.rs b/src/api/v4/federation/auth.rs index 44e06833..7d464e3b 100644 --- a/src/api/v4/federation/auth.rs +++ b/src/api/v4/federation/auth.rs @@ -21,6 +21,7 @@ use chrono::{Local, TimeDelta}; use std::collections::HashSet; use tracing::debug; use utoipa_axum::{router::OpenApiRouter, routes}; +use validator::Validate; use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}; use openidconnect::reqwest; @@ -80,6 +81,7 @@ pub async fn post( Path(idp_id): Path, Json(req): Json, ) -> Result { + req.validate()?; state .config .auth diff --git a/src/api/v4/federation/identity_provider/create.rs b/src/api/v4/federation/identity_provider/create.rs index cdb5ce98..416309a7 100644 --- a/src/api/v4/federation/identity_provider/create.rs +++ b/src/api/v4/federation/identity_provider/create.rs @@ -15,6 +15,7 @@ //! Identity providers: create IDP use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -53,6 +54,7 @@ pub(super) async fn create( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; policy .enforce( "identity/identity_provider_create", diff --git a/src/api/v4/federation/identity_provider/update.rs b/src/api/v4/federation/identity_provider/update.rs index c0426bbc..7e657c70 100644 --- a/src/api/v4/federation/identity_provider/update.rs +++ b/src/api/v4/federation/identity_provider/update.rs @@ -19,6 +19,7 @@ use axum::{ response::IntoResponse, }; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -58,6 +59,7 @@ pub(super) async fn update( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; // Fetch the current resource to pass current object into the policy evaluation let current = state .provider diff --git a/src/api/v4/federation/mapping/create.rs b/src/api/v4/federation/mapping/create.rs index 723394c2..18a51295 100644 --- a/src/api/v4/federation/mapping/create.rs +++ b/src/api/v4/federation/mapping/create.rs @@ -15,6 +15,7 @@ //! Federation attribute mapping: create use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -47,6 +48,7 @@ pub(super) async fn create( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; policy .enforce( "identity/mapping_create", diff --git a/src/api/v4/federation/mapping/update.rs b/src/api/v4/federation/mapping/update.rs index 40903b3a..760782af 100644 --- a/src/api/v4/federation/mapping/update.rs +++ b/src/api/v4/federation/mapping/update.rs @@ -19,6 +19,7 @@ use axum::{ response::IntoResponse, }; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -58,6 +59,7 @@ pub(super) async fn update( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; let current = state .provider .get_federation_provider() diff --git a/src/api/v4/federation/types/auth.rs b/src/api/v4/federation/types/auth.rs index 10633ea0..f47ffcb7 100644 --- a/src/api/v4/federation/types/auth.rs +++ b/src/api/v4/federation/types/auth.rs @@ -19,30 +19,36 @@ use axum::{ }; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use validator::Validate; /// Request for initializing the federated authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct IdentityProviderAuthRequest { /// Redirect URI to include in the auth request. + #[validate(url)] pub redirect_uri: String, /// IDP mapping id. + #[validate(length(max = 64))] pub mapping_id: Option, /// IDP mapping name. + #[validate(length(max = 64))] pub mapping_name: Option, /// Authentication scope. + #[validate(nested)] pub scope: Option, } /// Authentication initialization response. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct IdentityProviderAuthResponse { /// Url the client must open in the browser to continue the authentication. + #[validate(url)] pub auth_url: String, } /// Authentication callback request the user is sending to complete the /// authentication request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthCallbackParameters { /// Authentication state. pub state: String, diff --git a/src/api/v4/federation/types/identity_provider.rs b/src/api/v4/federation/types/identity_provider.rs index 931fe52a..dc2a595b 100644 --- a/src/api/v4/federation/types/identity_provider.rs +++ b/src/api/v4/federation/types/identity_provider.rs @@ -21,12 +21,13 @@ use derive_builder::Builder; use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::api::error::KeystoneApiError; use crate::federation::types; /// Identity provider data -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct IdentityProvider { /// The ID of the federated identity provider. @@ -95,18 +96,20 @@ pub struct IdentityProvider { } /// Identity provider response. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct IdentityProviderResponse { /// Identity provider object. + #[validate(nested)] pub identity_provider: IdentityProvider, } /// Identity provider data. -#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct IdentityProviderCreate { // TODO: add ID /// Identity provider name. + #[validate(length(max = 255))] pub name: String, /// The ID of the domain this identity provider belongs to. Empty value @@ -115,18 +118,21 @@ pub struct IdentityProviderCreate { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 64))] pub domain_id: Option, /// OIDC discovery endpoint for the identity provider. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(url, length(max = 255))] pub oidc_discovery_url: Option, /// The oidc `client_id` to use for the private client. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 255))] pub oidc_client_id: Option, /// The oidc `client_secret` to use for the private client. It is never @@ -134,12 +140,14 @@ pub struct IdentityProviderCreate { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 255))] pub oidc_client_secret: Option, /// The oidc response mode. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 64))] pub oidc_response_mode: Option, /// List of supported response types. @@ -154,6 +162,7 @@ pub struct IdentityProviderCreate { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(url)] pub jwks_url: Option, /// List of the jwt validation public keys. @@ -166,6 +175,7 @@ pub struct IdentityProviderCreate { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 255))] pub bound_issuer: Option, /// Default attribute mapping name which is automatically used when no @@ -174,6 +184,7 @@ pub struct IdentityProviderCreate { #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] #[schema(nullable = false)] + #[validate(length(max = 255))] pub default_mapping_name: Option, /// Additional special provider specific configuration @@ -185,26 +196,31 @@ pub struct IdentityProviderCreate { } /// New identity provider data. -#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct IdentityProviderUpdate { /// The new name of the federated identity provider. + #[validate(length(max = 255))] pub name: Option, /// The new OIDC discovery endpoint for the identity provider. #[builder(default)] + #[validate(url, length(max = 255))] pub oidc_discovery_url: Option>, /// The new oidc `client_id` to use for the private client. #[builder(default)] + #[validate(length(max = 255))] pub oidc_client_id: Option>, /// The new oidc `client_secret` to use for the private client. #[builder(default)] + #[validate(length(max = 255))] pub oidc_client_secret: Option>, /// The new oidc response mode. #[builder(default)] + #[validate(length(max = 255))] pub oidc_response_mode: Option>, /// The new oidc response mode. @@ -215,6 +231,7 @@ pub struct IdentityProviderUpdate { /// the provider does not provide discovery endpoint or when it is not /// standard compliant. #[builder(default)] + #[validate(url)] pub jwks_url: Option>, /// The list of the jwt validation public keys. @@ -223,6 +240,7 @@ pub struct IdentityProviderUpdate { /// The new bound issuer that is verified when using the identity provider. #[builder(default)] + #[validate(length(max = 255))] pub bound_issuer: Option>, /// New default attribute mapping name which is automatically used when no @@ -230,6 +248,7 @@ pub struct IdentityProviderUpdate { /// exist. #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] + #[validate(length(max = 255))] pub default_mapping_name: Option>, /// New additional provider configuration. @@ -239,18 +258,20 @@ pub struct IdentityProviderUpdate { } /// Identity provider create request -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct IdentityProviderCreateRequest { - /// Identity provider object + /// Identity provider object. + #[validate(nested)] pub identity_provider: IdentityProviderCreate, } /// Identity provider update request -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct IdentityProviderUpdateRequest { - /// Identity provider object + /// Identity provider object. + #[validate(nested)] pub identity_provider: IdentityProviderUpdate, } @@ -330,9 +351,10 @@ impl From for KeystoneApiError { } /// List of Identity Providers. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct IdentityProviderList { /// Collection of identity provider objects. + #[validate(nested)] pub identity_providers: Vec, } @@ -343,14 +365,16 @@ impl IntoResponse for IdentityProviderList { } /// Query parameters for listing federated identity providers. -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct IdentityProviderListParameters { /// Filters the response by IDP name. #[param(nullable = false)] + #[validate(length(max = 255))] pub name: Option, /// Filters the response by a domain ID. #[param(nullable = false)] + #[validate(length(max = 64))] pub domain_id: Option, } diff --git a/src/api/v4/federation/types/mapping.rs b/src/api/v4/federation/types/mapping.rs index 20feadd4..49734a3e 100644 --- a/src/api/v4/federation/types/mapping.rs +++ b/src/api/v4/federation/types/mapping.rs @@ -22,12 +22,13 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; +use validator::Validate; use crate::api::error::KeystoneApiError; use crate::federation::types; /// OIDC/JWT mapping data. -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct Mapping { /// Attribute mapping ID for federated logins. @@ -106,20 +107,23 @@ pub struct Mapping { pub token_restriction_id: Option, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct MappingResponse { /// IDP object + #[validate(nested)] pub mapping: Mapping, } /// OIDC/JWT attribute mapping create data. -#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct MappingCreate { /// Attribute mapping ID for federated logins. + #[validate(length(max = 64))] pub id: Option, /// Attribute mapping name for federated logins. + #[validate(length(max = 266))] pub name: String, /// `domain_id` owning the attribute mapping. @@ -131,10 +135,12 @@ pub struct MappingCreate { #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(length(max = 64))] pub domain_id: Option, /// ID of the federated identity provider for which this attribute mapping /// can be used. + #[validate(length(max = 64))] pub idp_id: String, /// Attribute mapping type ([oidc, jwt]). @@ -150,21 +156,25 @@ pub struct MappingCreate { pub allowed_redirect_uris: Option>, /// `user_id` claim name. + #[validate(length(max = 64))] pub user_id_claim: String, /// `user_name` claim name. + #[validate(length(max = 64))] pub user_name_claim: String, /// `domain_id` claim name. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(length(max = 64))] pub domain_id_claim: Option, /// `groups` claim name. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(length(max = 64))] pub groups_claim: Option, /// List of audiences that must be present in the token. @@ -177,6 +187,7 @@ pub struct MappingCreate { #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(length(max = 64))] pub bound_subject: Option, /// Additional claims that must be present in the token. @@ -195,20 +206,23 @@ pub struct MappingCreate { #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] #[schema(nullable = false)] + #[validate(length(max = 64))] pub token_project_id: Option, /// Token restrictions to be applied to the granted token. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub token_restriction_id: Option, } /// OIDC/JWT attribute mapping update data. -#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(into))] pub struct MappingUpdate { /// Attribute mapping name for federated logins. #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 255))] pub name: Option, /// `domain_id` owning the attribute mapping. @@ -219,11 +233,13 @@ pub struct MappingUpdate { /// provider is also shared (does not set the `domain_id` attribute). #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub domain_id: Option>, /// ID of the federated identity provider for which this attribute mapping /// can be used. #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub idp_id: Option, /// Attribute mapping type ([oidc, jwt]). @@ -240,30 +256,36 @@ pub struct MappingUpdate { /// `user_id` claim name. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub user_id_claim: Option, /// `user_name` claim name. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub user_name_claim: Option, #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub domain_id_claim: Option, /// `groups` claim name. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub groups_claim: Option>, /// List of audiences that must be present in the token. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub bound_audiences: Option>>, /// Token subject value that must be set in the token. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub bound_subject: Option>, /// Additional claims that must be present in the token. @@ -280,27 +302,31 @@ pub struct MappingUpdate { /// Fixed project_id for the token. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub token_project_id: Option>, /// Token restrictions to be applied to the granted token. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub token_restriction_id: Option, } /// OIDC/JWT attribute mapping create request. -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct MappingCreateRequest { /// Mapping object + #[validate(nested)] pub mapping: MappingCreate, } /// OIDC/JWT attribute mapping update request. -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct MappingUpdateRequest { /// Mapping object + #[validate(nested)] pub mapping: MappingUpdate, } @@ -432,18 +458,21 @@ impl IntoResponse for MappingList { } /// Query parameters for listing OIDC/JWT attribute mappings. -#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams, Validate)] pub struct MappingListParameters { /// Filters the response by IDP name. #[param(nullable = false)] + #[validate(length(max = 255))] pub name: Option, /// Filters the response by a domain ID. #[param(nullable = false)] + #[validate(length(max = 64))] pub domain_id: Option, /// Filters the response by a idp ID. #[param(nullable = false)] + #[validate(length(max = 64))] pub idp_id: Option, /// Filters the response by a mapping type. diff --git a/src/api/v4/token/restriction/create.rs b/src/api/v4/token/restriction/create.rs index 6eae2f72..c6d7e40f 100644 --- a/src/api/v4/token/restriction/create.rs +++ b/src/api/v4/token/restriction/create.rs @@ -15,6 +15,7 @@ use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -53,6 +54,7 @@ pub(super) async fn create( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; policy .enforce( "identity/token_restriction/create", diff --git a/src/api/v4/token/restriction/update.rs b/src/api/v4/token/restriction/update.rs index eaf47d61..bed916d2 100644 --- a/src/api/v4/token/restriction/update.rs +++ b/src/api/v4/token/restriction/update.rs @@ -19,6 +19,7 @@ use axum::{ response::IntoResponse, }; use mockall_double::double; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::KeystoneApiError; @@ -61,6 +62,7 @@ pub(super) async fn update( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; // Fetch the current resource to pass current object into the policy evaluation let current = state .provider diff --git a/src/api/v4/token/types/restriction.rs b/src/api/v4/token/types/restriction.rs index 092e5ff5..64310dc8 100644 --- a/src/api/v4/token/types/restriction.rs +++ b/src/api/v4/token/types/restriction.rs @@ -20,6 +20,7 @@ use axum::{ use derive_builder::Builder; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use validator::Validate; use crate::api::error::KeystoneApiError; use crate::api::v3::role_assignment::types::Role; @@ -30,7 +31,7 @@ use crate::token::types::{ }; /// Token restriction data. -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenRestriction { /// Allow token renew. @@ -40,28 +41,33 @@ pub struct TokenRestriction { pub allow_rescope: bool, /// Domain ID the token restriction belongs to. + #[validate(length(max = 64))] pub domain_id: String, /// Token restriction ID. + #[validate(length(max = 64))] pub id: String, /// Project ID that the token must be bound to. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub project_id: Option, /// User ID that the token must be bound to. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub user_id: Option, /// Bound token roles. #[builder(default)] + #[validate(nested)] pub roles: Vec, } /// New token restriction data. -#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenRestrictionCreate { /// Allow token renew. @@ -71,11 +77,13 @@ pub struct TokenRestrictionCreate { pub allow_rescope: bool, /// Domain ID the token restriction belongs to. + #[validate(length(max = 64))] pub domain_id: String, /// Project ID that the token must be bound to. #[builder(default)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub project_id: Option, /// User ID that the token must be bound to. @@ -85,11 +93,12 @@ pub struct TokenRestrictionCreate { /// Bound token roles. #[builder(default)] + #[validate(nested)] pub roles: Vec, } /// New token restriction data. -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[builder(setter(strip_option, into))] pub struct TokenRestrictionUpdate { /// Allow token renew. @@ -100,46 +109,55 @@ pub struct TokenRestrictionUpdate { /// Project ID that the token must be bound to. #[builder(default)] + #[validate(length(max = 64))] pub project_id: Option>, /// User ID that the token must be bound to. #[builder(default)] + #[validate(length(max = 64))] pub user_id: Option>, /// Bound token roles. #[builder(default)] + #[validate(nested)] pub roles: Option>, } /// Token restriction data. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct TokenRestrictionResponse { /// Restriction object. + #[validate(nested)] pub restriction: TokenRestriction, } /// Token restriction creation request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct TokenRestrictionCreateRequest { /// Restriction object. + #[validate(nested)] pub restriction: TokenRestrictionCreate, } /// Token restriction update request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct TokenRestrictionUpdateRequest { /// Restriction object. + #[validate(nested)] pub restriction: TokenRestrictionUpdate, } /// Token restriction list filters. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, IntoParams, Validate)] pub struct TokenRestrictionListParameters { /// Domain id. + #[validate(length(max = 64))] pub domain_id: Option, /// User id. + #[validate(length(max = 64))] pub user_id: Option, /// Project id. + #[validate(length(max = 64))] pub project_id: Option, } diff --git a/src/api/v4/user/passkey/register_finish.rs b/src/api/v4/user/passkey/register_finish.rs index 4d9d686d..ab1eda96 100644 --- a/src/api/v4/user/passkey/register_finish.rs +++ b/src/api/v4/user/passkey/register_finish.rs @@ -21,6 +21,7 @@ use axum::{ use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use mockall_double::double; use tracing::debug; +use validator::Validate; use crate::api::auth::Auth; use crate::api::error::{KeystoneApiError, WebauthnError}; @@ -61,6 +62,7 @@ pub(super) async fn finish( mut policy: Policy, Json(req): Json, ) -> Result { + req.validate()?; let user = state .provider .get_identity_provider() diff --git a/src/api/v4/user/passkey/register_start.rs b/src/api/v4/user/passkey/register_start.rs index 0d709f28..1d15f814 100644 --- a/src/api/v4/user/passkey/register_start.rs +++ b/src/api/v4/user/passkey/register_start.rs @@ -20,6 +20,7 @@ use axum::{ use base64::{Engine as _, engine::general_purpose::URL_SAFE}; use mockall_double::double; use tracing::debug; +use validator::Validate; use webauthn_rs::prelude::*; use crate::api::auth::Auth; @@ -70,6 +71,7 @@ pub(super) async fn start( State(state): State, Json(req): Json, ) -> Result { + req.validate()?; let user = state .provider .get_identity_provider() diff --git a/src/api/v4/user/types/passkey.rs b/src/api/v4/user/types/passkey.rs index c27f0763..f6fddaf3 100644 --- a/src/api/v4/user/types/passkey.rs +++ b/src/api/v4/user/types/passkey.rs @@ -16,32 +16,36 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use validator::Validate; /// Passkey registration request. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserPasskeyRegistrationStartRequest { /// The description for the passkey (name). + #[validate(nested)] pub passkey: PasskeyCreate, } /// Passkey information. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyCreate { /// Passkey description #[schema(nullable = false, max_length = 64)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 255))] pub description: Option, } /// Passkey. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PasskeyResponse { /// The description for the passkey (name). + #[validate(nested)] pub passkey: Passkey, } /// Passkey information. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct Passkey { /// Credential ID. pub credential_id: String, @@ -55,14 +59,15 @@ pub struct Passkey { /// /// This is the WebauthN challenge that need to be signed by the /// passkey/security device. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserPasskeyRegistrationStartResponse { /// The options. + #[validate(nested)] pub public_key: PublicKeyCredentialCreationOptions, } /// The requested options for the authentication. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PublicKeyCredentialCreationOptions { /// The requested attestation level from the device. #[schema(nullable = false)] @@ -75,6 +80,7 @@ pub struct PublicKeyCredentialCreationOptions { /// Criteria defining which authenticators may be used in this operation. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub authenticator_selection: Option, /// The challenge that should be signed by the authenticator. #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -82,37 +88,45 @@ pub struct PublicKeyCredentialCreationOptions { /// Credential ID’s that are excluded from being able to be registered. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub exclude_credentials: Option>, /// extensions. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub extensions: Option, /// Hints defining which types credentials may be used in this operation. #[serde(skip_serializing_if = "Option::is_none")] pub hints: Option>, /// The set of cryptographic types allowed by this server. + #[validate(nested)] pub pub_key_cred_params: Vec, /// The relying party + #[validate(nested)] pub rp: RelyingParty, /// The timeout for the authenticator in case of no interaction. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(range(min = 1))] pub timeout: Option, /// The user. + #[validate(nested)] pub user: User, } /// Relying Party Entity. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RelyingParty { /// The id of the relying party. + #[validate(length(max = 64))] pub id: String, /// The name of the relying party. + #[validate(length(max = 255))] pub name: String, } /// User Entity. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] #[schema(as = PasskeyUser)] pub struct User { /// The user’s id in base64 form. This MUST be a unique id, and must NOT @@ -122,14 +136,16 @@ pub struct User { pub id: String, /// A detailed name for the account, such as an email address. This value /// can change, so must not be used as a primary key. + #[validate(length(max = 255))] pub name: String, /// The user’s preferred name for display. This value can change, so must /// not be used as a primary key. + #[validate(length(max = 255))] pub display_name: String, } /// Public key cryptographic parameters -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PubKeyCredParams { /// The algorithm in use defined by CASE. pub alg: i64, @@ -138,7 +154,7 @@ pub struct PubKeyCredParams { } /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct PublicKeyCredentialDescriptor { /// The type of credential. pub type_: String, @@ -164,7 +180,7 @@ pub enum Mediation { } /// A descriptor of a credential that can be used. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AllowCredentials { /// The type of credential. pub type_: String, @@ -198,7 +214,7 @@ pub enum AuthenticatorTransport { } /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthenticatorSelectionCriteria { /// How the authenticator should be attached to the client machine. Note /// this is only a hint. It is not enforced in anyway shape or form. . @@ -302,7 +318,7 @@ pub enum AttestationFormat { /// Extension option inputs for PublicKeyCredentialCreationOptions. /// /// Implements `AuthenticatorExtensionsClientInputs` from the spec. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RequestRegistrationExtensions { /// ⚠️ - This extension result is always unsigned, and only indicates if the /// browser requests a residentKey to be created. It has no bearing on @@ -313,6 +329,7 @@ pub struct RequestRegistrationExtensions { /// The credProtect extension options. #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub cred_protect: Option, /// ⚠️ - Browsers support the creation of the secret, but not the retrieval /// of it. CTAP2.1 create hmac secret. @@ -332,7 +349,7 @@ pub struct RequestRegistrationExtensions { /// The desired options for the client’s use of the credProtect extension /// /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct CredProtect { /// The credential policy to enact. pub credential_protection_policy: CredentialProtectionPolicy, @@ -428,11 +445,12 @@ pub enum UserVerificationPolicy { /// You should not need to handle the inner content of this structure - you /// should provide this to the correctly handling function of Webauthn only. /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct UserPasskeyRegistrationFinishRequest { /// Optional credential description. #[schema(nullable = false, max_length = 64)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(length(max = 64))] pub description: Option, /// The id of the PublicKey credential, likely in base64. /// @@ -454,7 +472,7 @@ pub struct UserPasskeyRegistrationFinishRequest { } /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct AuthenticatorAttestationResponseRaw { /// . #[schema(value_type = String, format = Binary, content_encoding = "base64")] @@ -470,7 +488,7 @@ pub struct AuthenticatorAttestationResponseRaw { /// The default /// option here for Options are None, so it can be derived -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct RegistrationExtensionsClientOutputs { /// Indicates whether the client used the provided appid extension. #[schema(nullable = false)] @@ -481,6 +499,7 @@ pub struct RegistrationExtensionsClientOutputs { /// be trusted! #[schema(nullable = false)] #[serde(skip_serializing_if = "Option::is_none")] + #[validate(nested)] pub cred_props: Option, /// Indicates if the client successfully applied a HMAC Secret. #[schema(nullable = false)] @@ -498,7 +517,7 @@ pub struct RegistrationExtensionsClientOutputs { } /// -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema, Validate)] pub struct CredProps { /// A user agent supplied hint that this credential may have created a /// resident key. It is returned from the user agent, not the diff --git a/src/token/mod.rs b/src/token/mod.rs index 7474f4bf..ecb65811 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -328,138 +328,88 @@ impl TokenProvider { } Ok(()) } -} -#[async_trait] -impl TokenApi for TokenProvider { - /// Authenticate by token - #[tracing::instrument(level = "info", skip(self, state, credential))] - async fn authenticate_by_token<'a>( + /// Expand the target scope information in the token. + async fn expand_scope_information( &self, state: &ServiceState, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - ) -> Result { - // TODO: is the expand really false? - let token = self - .validate_token( - state, - credential, - allow_expired, - window_seconds, - Some(false), - ) - .await?; - if let Token::Restricted(restriction) = &token - && !restriction.allow_renew - { - return Err(AuthenticationError::TokenRenewalForbidden)?; - } - let mut auth_info_builder = AuthenticatedInfo::builder(); - auth_info_builder.user_id(token.user_id()); - auth_info_builder.methods(token.methods().clone()); - auth_info_builder.audit_ids(token.audit_ids().clone()); - if let Token::Restricted(restriction) = &token { - auth_info_builder.token_restriction_id(restriction.token_restriction_id.clone()); - } - Ok(auth_info_builder - .build() - .map_err(AuthenticationError::from)?) - } - - /// Validate token - #[tracing::instrument(level = "info", skip(self, state, credential))] - async fn validate_token<'a>( - &self, - state: &ServiceState, - credential: &'a str, - allow_expired: Option, - window_seconds: Option, - expand: Option, - ) -> Result { - let mut token = self.backend_driver.decode(credential)?; - if Local::now().to_utc() - > token - .expires_at() - .checked_add_signed(TimeDelta::seconds(window_seconds.unwrap_or(0))) - .unwrap_or_else(|| *token.expires_at()) - && !allow_expired.unwrap_or(false) - { - return Err(TokenProviderError::Expired); - } - - // Expand the token unless `expand = Some(false)` - if expand.is_none_or(|v| v) { - token = self.expand_token_information(state, &token).await?; - } - - if state - .provider - .get_revoke_provider() - .is_token_revoked(state, &token) - .await? - { - return Err(TokenProviderError::TokenRevoked); - } + token: &mut Token, + ) -> Result<(), TokenProviderError> { + match token { + Token::ProjectScope(data) => { + if data.project.is_none() { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; - Ok(token) - } + data.project = project; + } + } + Token::ApplicationCredential(data) => { + if data.project.is_none() { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; - #[tracing::instrument(level = "debug", skip(self))] - fn issue_token( - &self, - authentication_info: AuthenticatedInfo, - authz_info: AuthzInfo, - token_restrictions: Option<&TokenRestriction>, - ) -> Result { - // This should be executed already, but let's better repeat it as last line of - // defence. It is also necessary to call this before to stop before we - // start to resolve authz info. - authentication_info.validate()?; + data.project = project; + } + } + Token::FederationProjectScope(data) => { + if data.project.is_none() { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; - // TODO: Check whether it is allowed to change the scope of the token if - // AuthenticatedInfo already contains scope it was issued for. - let mut authentication_info = authentication_info; - authentication_info.audit_ids.push( - URL_SAFE - .encode(Uuid::new_v4().as_bytes()) - .trim_end_matches('=') - .to_string(), - ); - if let Some(token_restrictions) = &token_restrictions { - self.create_restricted_token(&authentication_info, &authz_info, token_restrictions) - } else if authentication_info.idp_id.is_some() && authentication_info.protocol_id.is_some() - { - match &authz_info { - AuthzInfo::Project(project) => { - self.create_federated_project_scope_token(&authentication_info, project) + data.project = project; } - AuthzInfo::Domain(domain) => { - self.create_federated_domain_scope_token(&authentication_info, domain) + } + Token::DomainScope(data) => { + if data.domain.is_none() { + let domain = state + .provider + .get_resource_provider() + .get_domain(state, &data.domain_id) + .await?; + + data.domain = domain; } - AuthzInfo::Unscoped => self.create_federated_unscoped_token(&authentication_info), } - } else { - match &authz_info { - AuthzInfo::Project(project) => { - self.create_project_scope_token(&authentication_info, project) + Token::FederationDomainScope(data) => { + if data.domain.is_none() { + let domain = state + .provider + .get_resource_provider() + .get_domain(state, &data.domain_id) + .await?; + + data.domain = domain; } - AuthzInfo::Domain(domain) => { - self.create_domain_scope_token(&authentication_info, domain) + } + Token::Restricted(data) => { + if data.project.is_none() { + let project = state + .provider + .get_resource_provider() + .get_project(state, &data.project_id) + .await?; + + data.project = project; } - AuthzInfo::Unscoped => self.create_unscoped_token(&authentication_info), } - } - } - /// Validate token - fn encode_token(&self, token: &Token) -> Result { - self.backend_driver.encode(token) + _ => {} + }; + Ok(()) } - /// Populate role assignments in the token that support that information - async fn populate_role_assignments( + /// Populate role assignments in the token that support that information. + async fn _populate_role_assignments( &self, state: &ServiceState, token: &mut Token, @@ -618,84 +568,159 @@ impl TokenApi for TokenProvider { Ok(()) } +} - async fn expand_token_information( +#[async_trait] +impl TokenApi for TokenProvider { + /// Authenticate by token. + #[tracing::instrument(level = "info", skip(self, state, credential))] + async fn authenticate_by_token<'a>( &self, state: &ServiceState, - token: &Token, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + ) -> Result { + // TODO: is the expand really false? + let token = self + .validate_token( + state, + credential, + allow_expired, + window_seconds, + Some(false), + ) + .await?; + if let Token::Restricted(restriction) = &token + && !restriction.allow_renew + { + return Err(AuthenticationError::TokenRenewalForbidden)?; + } + let mut auth_info_builder = AuthenticatedInfo::builder(); + auth_info_builder.user_id(token.user_id()); + auth_info_builder.methods(token.methods().clone()); + auth_info_builder.audit_ids(token.audit_ids().clone()); + if let Token::Restricted(restriction) = &token { + auth_info_builder.token_restriction_id(restriction.token_restriction_id.clone()); + } + Ok(auth_info_builder + .build() + .map_err(AuthenticationError::from)?) + } + + /// Validate token. + #[tracing::instrument(level = "info", skip(self, state, credential))] + async fn validate_token<'a>( + &self, + state: &ServiceState, + credential: &'a str, + allow_expired: Option, + window_seconds: Option, + expand: Option, ) -> Result { - let mut new_token = token.clone(); - match new_token { - Token::ProjectScope(ref mut data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; + let mut token = self.backend_driver.decode(credential)?; + if Local::now().to_utc() + > token + .expires_at() + .checked_add_signed(TimeDelta::seconds(window_seconds.unwrap_or(0))) + .unwrap_or_else(|| *token.expires_at()) + && !allow_expired.unwrap_or(false) + { + return Err(TokenProviderError::Expired); + } - data.project = project; - } - } - Token::ApplicationCredential(ref mut data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; + // Expand the token unless `expand = Some(false)` + if expand.is_none_or(|v| v) { + token = self.expand_token_information(state, &token).await?; + } - data.project = project; - } - } - Token::FederationProjectScope(ref mut data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; + if state + .provider + .get_revoke_provider() + .is_token_revoked(state, &token) + .await? + { + return Err(TokenProviderError::TokenRevoked); + } - data.project = project; - } - } - Token::DomainScope(ref mut data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; + Ok(token) + } - data.domain = domain; - } - } - Token::FederationDomainScope(ref mut data) => { - if data.domain.is_none() { - let domain = state - .provider - .get_resource_provider() - .get_domain(state, &data.domain_id) - .await?; + #[tracing::instrument(level = "debug", skip(self))] + fn issue_token( + &self, + authentication_info: AuthenticatedInfo, + authz_info: AuthzInfo, + token_restrictions: Option<&TokenRestriction>, + ) -> Result { + // This should be executed already, but let's better repeat it as last line of + // defence. It is also necessary to call this before to stop before we + // start to resolve authz info. + authentication_info.validate()?; - data.domain = domain; + // TODO: Check whether it is allowed to change the scope of the token if + // AuthenticatedInfo already contains scope it was issued for. + let mut authentication_info = authentication_info; + authentication_info.audit_ids.push( + URL_SAFE + .encode(Uuid::new_v4().as_bytes()) + .trim_end_matches('=') + .to_string(), + ); + if let Some(token_restrictions) = &token_restrictions { + self.create_restricted_token(&authentication_info, &authz_info, token_restrictions) + } else if authentication_info.idp_id.is_some() && authentication_info.protocol_id.is_some() + { + match &authz_info { + AuthzInfo::Project(project) => { + self.create_federated_project_scope_token(&authentication_info, project) + } + AuthzInfo::Domain(domain) => { + self.create_federated_domain_scope_token(&authentication_info, domain) } + AuthzInfo::Unscoped => self.create_federated_unscoped_token(&authentication_info), } - Token::Restricted(ref mut data) => { - if data.project.is_none() { - let project = state - .provider - .get_resource_provider() - .get_project(state, &data.project_id) - .await?; - - data.project = project; + } else { + match &authz_info { + AuthzInfo::Project(project) => { + self.create_project_scope_token(&authentication_info, project) + } + AuthzInfo::Domain(domain) => { + self.create_domain_scope_token(&authentication_info, domain) } + AuthzInfo::Unscoped => self.create_unscoped_token(&authentication_info), } + } + } - _ => {} - }; + /// Encode the token into a `String` representation. + /// + /// Encode the [`Token`] into the `String` to be used as a http header. + fn encode_token(&self, token: &Token) -> Result { + self.backend_driver.encode(token) + } + + /// Populate role assignments in the token that support that information. + async fn populate_role_assignments( + &self, + state: &ServiceState, + token: &mut Token, + ) -> Result<(), TokenProviderError> { + self._populate_role_assignments(state, token).await + } + + /// Expand the token information. + /// + /// Query and expand information about the user, scope and the role + /// assignments into the token. + async fn expand_token_information( + &self, + state: &ServiceState, + token: &Token, + ) -> Result { + let mut new_token = token.clone(); self.expand_user_information(state, &mut new_token).await?; + self.expand_scope_information(state, &mut new_token).await?; self.populate_role_assignments(state, &mut new_token) .await?; Ok(new_token)