diff --git a/Cargo.lock b/Cargo.lock index 729925cc2f3ff..bbb9b9eebda9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3383,6 +3383,7 @@ dependencies = [ "opendal", "ordered-float 4.2.0", "parking_lot 0.12.1", + "passwords", "percent-encoding", "regex", "roaring", @@ -9510,6 +9511,15 @@ dependencies = [ "regex", ] +[[package]] +name = "passwords" +version = "3.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11407193a7c2bd14ec6b0ec3394da6fdcf7a4d5dcbc8c3cc38dfb17802c8d59c" +dependencies = [ + "random-pick", +] + [[package]] name = "paste" version = "1.0.12" @@ -10620,6 +10630,37 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "random-number" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3da5cbb4c27c5150c03a54a7e4745437cd90f9e329ae657c0b889a144bb7be" +dependencies = [ + "proc-macro-hack", + "rand 0.8.5", + "random-number-macro-impl", +] + +[[package]] +name = "random-number-macro-impl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b86292cf41ccfc96c5de7165c1c53d5b4ac540c5bab9d1857acbe9eba5f1a0b" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "random-pick" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c179499072da789afe44127d5f4aa6012de2c2f96ef759990196b37387a2a0f8" +dependencies = [ + "random-number", +] + [[package]] name = "raw-cpuid" version = "10.7.0" diff --git a/src/common/exception/src/exception_code.rs b/src/common/exception/src/exception_code.rs index 137caee6372c7..350fda73bdebc 100644 --- a/src/common/exception/src/exception_code.rs +++ b/src/common/exception/src/exception_code.rs @@ -225,6 +225,11 @@ build_exceptions! { NetworkPolicyAlreadyExists(2208), IllegalNetworkPolicy(2209), NetworkPolicyIsUsedByUser(2210), + UnknownPasswordPolicy(2211), + PasswordPolicyAlreadyExists(2212), + IllegalPasswordPolicy(2213), + PasswordPolicyIsUsedByUser(2214), + InvalidPassword(2215), // Meta api error codes. DatabaseAlreadyExists(2301), diff --git a/src/meta/app/src/principal/mod.rs b/src/meta/app/src/principal/mod.rs index d4a137c72e179..5fb35d7b67962 100644 --- a/src/meta/app/src/principal/mod.rs +++ b/src/meta/app/src/principal/mod.rs @@ -18,6 +18,7 @@ mod connection; mod file_format; mod network_policy; mod ownership_info; +mod password_policy; mod principal_identity; mod role_info; mod user_auth; @@ -35,6 +36,7 @@ pub use connection::*; pub use file_format::*; pub use network_policy::NetworkPolicy; pub use ownership_info::OwnershipInfo; +pub use password_policy::PasswordPolicy; pub use principal_identity::PrincipalIdentity; pub use role_info::RoleInfo; pub use role_info::RoleInfoSerdeError; diff --git a/src/meta/app/src/principal/password_policy.rs b/src/meta/app/src/principal/password_policy.rs new file mode 100644 index 0000000000000..7b22e64c75e31 --- /dev/null +++ b/src/meta/app/src/principal/password_policy.rs @@ -0,0 +1,35 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::DateTime; +use chrono::Utc; + +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Eq, PartialEq, Default)] +pub struct PasswordPolicy { + pub name: String, + pub min_length: u64, + pub max_length: u64, + pub min_upper_case_chars: u64, + pub min_lower_case_chars: u64, + pub min_numeric_chars: u64, + pub min_special_chars: u64, + pub min_age_days: u64, + pub max_age_days: u64, + pub max_retries: u64, + pub lockout_time_mins: u64, + pub history: u64, + pub comment: String, + pub create_on: DateTime<Utc>, + pub update_on: Option<DateTime<Utc>>, +} diff --git a/src/meta/app/src/principal/user_info.rs b/src/meta/app/src/principal/user_info.rs index 6f8d31f12a1cb..8906b339cebb2 100644 --- a/src/meta/app/src/principal/user_info.rs +++ b/src/meta/app/src/principal/user_info.rs @@ -107,6 +107,8 @@ pub struct UserOption { default_role: Option<String>, network_policy: Option<String>, + + password_policy: Option<String>, } impl UserOption { @@ -115,6 +117,7 @@ impl UserOption { flags, default_role: None, network_policy: None, + password_policy: None, } } @@ -137,6 +140,11 @@ impl UserOption { self } + pub fn with_password_policy(mut self, password_policy: Option<String>) -> Self { + self.password_policy = password_policy; + self + } + pub fn with_set_flag(mut self, flag: UserOptionFlag) -> Self { self.flags.insert(flag); self @@ -154,6 +162,10 @@ impl UserOption { self.network_policy.as_ref() } + pub fn password_policy(&self) -> Option<&String> { + self.password_policy.as_ref() + } + pub fn set_default_role(&mut self, default_role: Option<String>) { self.default_role = default_role; } @@ -162,6 +174,10 @@ impl UserOption { self.network_policy = network_policy; } + pub fn set_password_policy(&mut self, password_policy: Option<String>) { + self.password_policy = password_policy; + } + pub fn set_all_flag(&mut self) { self.flags = BitFlags::all(); } diff --git a/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs b/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs index 44e82bd93ffea..97580c447d174 100644 --- a/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs +++ b/src/meta/proto-conv/src/user_from_to_protobuf_impl.rs @@ -99,7 +99,8 @@ impl FromToProto for mt::principal::UserOption { Ok(mt::principal::UserOption::default() .with_flags(flags) .with_default_role(p.default_role) - .with_network_policy(p.network_policy)) + .with_network_policy(p.network_policy) + .with_password_policy(p.password_policy)) } fn to_pb(&self) -> Result<pb::UserOption, Incompatible> { @@ -109,6 +110,7 @@ impl FromToProto for mt::principal::UserOption { flags: self.flags().bits(), default_role: self.default_role().cloned(), network_policy: self.network_policy().cloned(), + password_policy: self.password_policy().cloned(), }) } } @@ -389,3 +391,59 @@ impl FromToProto for mt::principal::NetworkPolicy { }) } } + +impl FromToProto for mt::principal::PasswordPolicy { + type PB = pb::PasswordPolicy; + fn get_pb_ver(p: &Self::PB) -> u64 { + p.ver + } + fn from_pb(p: pb::PasswordPolicy) -> Result<Self, Incompatible> + where Self: Sized { + reader_check_msg(p.ver, p.min_reader_ver)?; + Ok(mt::principal::PasswordPolicy { + name: p.name.clone(), + min_length: p.min_length, + max_length: p.max_length, + min_upper_case_chars: p.min_upper_case_chars, + min_lower_case_chars: p.min_lower_case_chars, + min_numeric_chars: p.min_numeric_chars, + min_special_chars: p.min_special_chars, + min_age_days: p.min_age_days, + max_age_days: p.max_age_days, + max_retries: p.max_retries, + lockout_time_mins: p.lockout_time_mins, + history: p.history, + comment: p.comment, + create_on: DateTime::<Utc>::from_pb(p.create_on)?, + update_on: match p.update_on { + Some(t) => Some(DateTime::<Utc>::from_pb(t)?), + None => None, + }, + }) + } + + fn to_pb(&self) -> Result<pb::PasswordPolicy, Incompatible> { + Ok(pb::PasswordPolicy { + ver: VER, + min_reader_ver: MIN_READER_VER, + name: self.name.clone(), + min_length: self.min_length, + max_length: self.max_length, + min_upper_case_chars: self.min_upper_case_chars, + min_lower_case_chars: self.min_lower_case_chars, + min_numeric_chars: self.min_numeric_chars, + min_special_chars: self.min_special_chars, + min_age_days: self.min_age_days, + max_age_days: self.max_age_days, + max_retries: self.max_retries, + lockout_time_mins: self.lockout_time_mins, + history: self.history, + comment: self.comment.clone(), + create_on: self.create_on.to_pb()?, + update_on: match &self.update_on { + Some(t) => Some(t.to_pb()?), + None => None, + }, + }) + } +} diff --git a/src/meta/proto-conv/src/util.rs b/src/meta/proto-conv/src/util.rs index f4b2b6299c849..b69d88460e4b9 100644 --- a/src/meta/proto-conv/src/util.rs +++ b/src/meta/proto-conv/src/util.rs @@ -96,6 +96,7 @@ const META_CHANGE_LOG: &[(u64, &str)] = &[ (64, "2023-11-16: Add: user.proto/NDJsonFileFormatParams add field `missing_field_as` and `null_field_as`", ), (65, "2023-11-16: Retype: use Datetime<Utc> instead of u64 to in lvt.time", ), (66, "2023-12-15: Add: stage.proto/StageInfo::created_on", ), + (67, "2023-12-19: Add: user.proto/PasswordPolicy and UserOption::password_policy", ), // Dear developer: // If you're gonna add a new metadata version, you'll have to add a test for it. // You could just copy an existing test file(e.g., `../tests/it/v024_table_meta.rs`) diff --git a/src/meta/proto-conv/tests/it/main.rs b/src/meta/proto-conv/tests/it/main.rs index 3bdd6e8eafa82..90ace49427979 100644 --- a/src/meta/proto-conv/tests/it/main.rs +++ b/src/meta/proto-conv/tests/it/main.rs @@ -70,3 +70,4 @@ mod v063_connection; mod v064_ndjson_format_params; mod v065_least_visible_time; mod v066_stage_create_on; +mod v067_password_policy; diff --git a/src/meta/proto-conv/tests/it/v067_password_policy.rs b/src/meta/proto-conv/tests/it/v067_password_policy.rs new file mode 100644 index 0000000000000..3671c39ef67c7 --- /dev/null +++ b/src/meta/proto-conv/tests/it/v067_password_policy.rs @@ -0,0 +1,110 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use chrono::TimeZone; +use chrono::Utc; +use databend_common_meta_app::principal::UserPrivilegeType; +use enumflags2::make_bitflags; +use minitrace::func_name; + +use crate::common; + +// These bytes are built when a new version in introduced, +// and are kept for backward compatibility test. +// +// ************************************************************* +// * These messages should never be updated, * +// * only be added when a new version is added, * +// * or be removed when an old version is no longer supported. * +// ************************************************************* +// +// The message bytes are built from the output of `test_build_pb_buf()` +#[test] +fn test_decode_v67_password_policy() -> anyhow::Result<()> { + // password policy + let bytes: Vec<u8> = vec![ + 10, 19, 116, 101, 115, 116, 112, 97, 115, 115, 119, 111, 114, 100, 112, 111, 108, 105, 99, + 121, 49, 16, 8, 24, 30, 32, 1, 40, 2, 48, 3, 56, 4, 64, 10, 72, 90, 80, 5, 88, 10, 96, 1, + 106, 12, 115, 111, 109, 101, 32, 99, 111, 109, 109, 101, 110, 116, 114, 23, 50, 48, 49, 52, + 45, 49, 49, 45, 50, 56, 32, 49, 50, 58, 48, 48, 58, 48, 57, 32, 85, 84, 67, 122, 23, 50, + 48, 49, 52, 45, 49, 49, 45, 50, 56, 32, 49, 50, 58, 48, 48, 58, 48, 57, 32, 85, 84, 67, + 160, 6, 67, 168, 6, 24, + ]; + + let want = || databend_common_meta_app::principal::PasswordPolicy { + name: "testpasswordpolicy1".to_string(), + min_length: 8, + max_length: 30, + min_upper_case_chars: 1, + min_lower_case_chars: 2, + min_numeric_chars: 3, + min_special_chars: 4, + min_age_days: 10, + max_age_days: 90, + max_retries: 5, + lockout_time_mins: 10, + history: 1, + comment: "some comment".to_string(), + create_on: Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap(), + update_on: Some(Utc.with_ymd_and_hms(2014, 11, 28, 12, 0, 9).unwrap()), + }; + + common::test_pb_from_to(func_name!(), want())?; + common::test_load_old(func_name!(), bytes.as_slice(), 67, want())?; + + // user info with password policy + let bytes: Vec<u8> = vec![ + 10, 9, 116, 101, 115, 116, 95, 117, 115, 101, 114, 18, 1, 37, 26, 25, 18, 17, 10, 13, 116, + 101, 115, 116, 95, 112, 97, 115, 115, 119, 111, 114, 100, 16, 1, 160, 6, 67, 168, 6, 24, + 34, 26, 10, 18, 10, 8, 10, 0, 160, 6, 67, 168, 6, 24, 16, 2, 160, 6, 67, 168, 6, 24, 160, + 6, 67, 168, 6, 24, 42, 15, 8, 10, 16, 128, 80, 24, 128, 160, 1, 160, 6, 67, 168, 6, 24, 50, + 46, 8, 1, 18, 5, 114, 111, 108, 101, 49, 26, 8, 109, 121, 112, 111, 108, 105, 99, 121, 34, + 19, 116, 101, 115, 116, 112, 97, 115, 115, 119, 111, 114, 100, 112, 111, 108, 105, 99, 121, + 49, 160, 6, 67, 168, 6, 24, 160, 6, 67, 168, 6, 24, + ]; + + let want = || databend_common_meta_app::principal::UserInfo { + name: "test_user".to_string(), + hostname: "%".to_string(), + auth_info: databend_common_meta_app::principal::AuthInfo::Password { + hash_value: [ + 116, 101, 115, 116, 95, 112, 97, 115, 115, 119, 111, 114, 100, + ] + .to_vec(), + hash_method: databend_common_meta_app::principal::PasswordHashMethod::DoubleSha1, + }, + grants: databend_common_meta_app::principal::UserGrantSet::new( + vec![databend_common_meta_app::principal::GrantEntry::new( + databend_common_meta_app::principal::GrantObject::Global, + make_bitflags!(UserPrivilegeType::{Create}), + )], + HashSet::new(), + ), + quota: databend_common_meta_app::principal::UserQuota { + max_cpu: 10, + max_memory_in_bytes: 10240, + max_storage_in_bytes: 20480, + }, + option: databend_common_meta_app::principal::UserOption::default() + .with_set_flag(databend_common_meta_app::principal::UserOptionFlag::TenantSetting) + .with_default_role(Some("role1".into())) + .with_network_policy(Some("mypolicy".to_string())) + .with_password_policy(Some("testpasswordpolicy1".to_string())), + }; + + common::test_pb_from_to(func_name!(), want())?; + common::test_load_old(func_name!(), bytes.as_slice(), 67, want()) +} diff --git a/src/meta/protos/proto/user.proto b/src/meta/protos/proto/user.proto index b5cc639d33391..b7f5408b4bd49 100644 --- a/src/meta/protos/proto/user.proto +++ b/src/meta/protos/proto/user.proto @@ -106,6 +106,7 @@ message UserOption { uint64 flags = 1; optional string default_role = 2; optional string network_policy = 3; + optional string password_policy = 4; } message UserInfo { @@ -139,3 +140,24 @@ message NetworkPolicy { string create_on = 5; optional string update_on = 6; } + +message PasswordPolicy { + uint64 ver = 100; + uint64 min_reader_ver = 101; + + string name = 1; + uint64 min_length = 2; + uint64 max_length = 3; + uint64 min_upper_case_chars = 4; + uint64 min_lower_case_chars = 5; + uint64 min_numeric_chars = 6; + uint64 min_special_chars = 7; + uint64 min_age_days = 8; + uint64 max_age_days = 9; + uint64 max_retries = 10; + uint64 lockout_time_mins = 11; + uint64 history = 12; + string comment = 13; + string create_on = 14; + optional string update_on = 15; +} diff --git a/src/query/ast/src/ast/format/ast_format.rs b/src/query/ast/src/ast/format/ast_format.rs index dc471b28839f4..b924fd941a5fe 100644 --- a/src/query/ast/src/ast/format/ast_format.rs +++ b/src/query/ast/src/ast/format/ast_format.rs @@ -2604,6 +2604,50 @@ impl<'ast> Visitor<'ast> for AstFormatVisitor { self.children.push(node); } + fn visit_create_password_policy(&mut self, stmt: &'ast CreatePasswordPolicyStmt) { + let ctx = AstFormatContext::new(format!("PasswordPolicyName {}", stmt.name)); + let child = FormatTreeNode::new(ctx); + + let name = "CreatePasswordPolicy".to_string(); + let format_ctx = AstFormatContext::with_children(name, 1); + let node = FormatTreeNode::with_children(format_ctx, vec![child]); + self.children.push(node); + } + + fn visit_alter_password_policy(&mut self, stmt: &'ast AlterPasswordPolicyStmt) { + let ctx = AstFormatContext::new(format!("PasswordPolicyName {}", stmt.name)); + let child = FormatTreeNode::new(ctx); + + let name = "AlterPasswordPolicy".to_string(); + let format_ctx = AstFormatContext::with_children(name, 1); + let node = FormatTreeNode::with_children(format_ctx, vec![child]); + self.children.push(node); + } + + fn visit_drop_password_policy(&mut self, stmt: &'ast DropPasswordPolicyStmt) { + let ctx = AstFormatContext::new(format!("PasswordPolicyName {}", stmt.name)); + let child = FormatTreeNode::new(ctx); + + let name = "DropPasswordPolicy".to_string(); + let format_ctx = AstFormatContext::with_children(name, 1); + let node = FormatTreeNode::with_children(format_ctx, vec![child]); + self.children.push(node); + } + + fn visit_desc_password_policy(&mut self, stmt: &'ast DescPasswordPolicyStmt) { + let ctx = AstFormatContext::new(format!("PasswordPolicyName {}", stmt.name)); + let child = FormatTreeNode::new(ctx); + + let name = "DescPasswordPolicy".to_string(); + let format_ctx = AstFormatContext::with_children(name, 1); + let node = FormatTreeNode::with_children(format_ctx, vec![child]); + self.children.push(node); + } + + fn visit_show_password_policies(&mut self, show_options: &'ast Option<ShowOptions>) { + self.visit_show_options(show_options, "ShowPasswordPolicies".to_string()); + } + fn visit_with(&mut self, with: &'ast With) { let mut children = Vec::with_capacity(with.ctes.len()); for cte in with.ctes.iter() { diff --git a/src/query/ast/src/ast/statements/mod.rs b/src/query/ast/src/ast/statements/mod.rs index dd550cf0c3d00..a0482e2f349b1 100644 --- a/src/query/ast/src/ast/statements/mod.rs +++ b/src/query/ast/src/ast/statements/mod.rs @@ -28,6 +28,7 @@ mod kill; mod lock; mod merge_into; mod network_policy; +mod password_policy; mod pipe; mod presign; mod replace; @@ -61,6 +62,7 @@ pub use kill::*; pub use lock::*; pub use merge_into::*; pub use network_policy::*; +pub use password_policy::*; pub use pipe::*; pub use presign::*; pub use replace::*; diff --git a/src/query/ast/src/ast/statements/password_policy.rs b/src/query/ast/src/ast/statements/password_policy.rs new file mode 100644 index 0000000000000..b1e51c51893da --- /dev/null +++ b/src/query/ast/src/ast/statements/password_policy.rs @@ -0,0 +1,234 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::fmt::Display; +use std::fmt::Formatter; + +#[derive(Debug, Clone, PartialEq)] +pub struct CreatePasswordPolicyStmt { + pub if_not_exists: bool, + pub name: String, + pub set_options: PasswordSetOptions, +} + +impl Display for CreatePasswordPolicyStmt { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "CREATE PASSWORD POLICY ")?; + if self.if_not_exists { + write!(f, "IF NOT EXISTS ")?; + } + write!(f, "{}", self.name)?; + write!(f, "{}", self.set_options)?; + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AlterPasswordPolicyStmt { + pub if_exists: bool, + pub name: String, + pub action: AlterPasswordAction, +} + +impl Display for AlterPasswordPolicyStmt { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "ALTER PASSWORD POLICY ")?; + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + write!(f, "{} ", self.name)?; + write!(f, "{}", self.action)?; + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum AlterPasswordAction { + SetOptions(PasswordSetOptions), + UnSetOptions(PasswordUnSetOptions), +} + +impl Display for AlterPasswordAction { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + AlterPasswordAction::SetOptions(set_options) => { + write!(f, "SET {}", set_options)?; + } + AlterPasswordAction::UnSetOptions(unset_options) => { + write!(f, "UNSET {}", unset_options)?; + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PasswordSetOptions { + pub min_length: Option<u64>, + pub max_length: Option<u64>, + pub min_upper_case_chars: Option<u64>, + pub min_lower_case_chars: Option<u64>, + pub min_numeric_chars: Option<u64>, + pub min_special_chars: Option<u64>, + pub min_age_days: Option<u64>, + pub max_age_days: Option<u64>, + pub max_retries: Option<u64>, + pub lockout_time_mins: Option<u64>, + pub history: Option<u64>, + pub comment: Option<String>, +} + +impl Display for PasswordSetOptions { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + if let Some(min_length) = self.min_length { + write!(f, " PASSWORD_MIN_LENGTH = {}", min_length)?; + } + if let Some(max_length) = self.max_length { + write!(f, " PASSWORD_MAX_LENGTH = {}", max_length)?; + } + if let Some(min_upper_case_chars) = self.min_upper_case_chars { + write!( + f, + " PASSWORD_MIN_UPPER_CASE_CHARS = {}", + min_upper_case_chars + )?; + } + if let Some(min_lower_case_chars) = self.min_lower_case_chars { + write!( + f, + " PASSWORD_MIN_LOWER_CASE_CHARS = {}", + min_lower_case_chars + )?; + } + if let Some(min_numeric_chars) = self.min_numeric_chars { + write!(f, " PASSWORD_MIN_NUMERIC_CHARS = {}", min_numeric_chars)?; + } + if let Some(min_special_chars) = self.min_special_chars { + write!(f, " PASSWORD_MIN_SPECIAL_CHARS = {}", min_special_chars)?; + } + if let Some(min_age_days) = self.min_age_days { + write!(f, " PASSWORD_MIN_AGE_DAYS = {}", min_age_days)?; + } + if let Some(max_age_days) = self.max_age_days { + write!(f, " PASSWORD_MAX_AGE_DAYS = {}", max_age_days)?; + } + if let Some(max_retries) = self.max_retries { + write!(f, " PASSWORD_MAX_RETRIES = {}", max_retries)?; + } + if let Some(lockout_time_mins) = self.lockout_time_mins { + write!(f, " PASSWORD_LOCKOUT_TIME_MINS = {}", lockout_time_mins)?; + } + if let Some(history) = self.history { + write!(f, " PASSWORD_HISTORY = {}", history)?; + } + if let Some(comment) = &self.comment { + write!(f, " COMMENT = '{}'", comment)?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PasswordUnSetOptions { + pub min_length: bool, + pub max_length: bool, + pub min_upper_case_chars: bool, + pub min_lower_case_chars: bool, + pub min_numeric_chars: bool, + pub min_special_chars: bool, + pub min_age_days: bool, + pub max_age_days: bool, + pub max_retries: bool, + pub lockout_time_mins: bool, + pub history: bool, + pub comment: bool, +} + +impl Display for PasswordUnSetOptions { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + if self.min_length { + write!(f, " PASSWORD_MIN_LENGTH")?; + } + if self.max_length { + write!(f, " PASSWORD_MAX_LENGTH")?; + } + if self.min_upper_case_chars { + write!(f, " PASSWORD_MIN_UPPER_CASE_CHARS")?; + } + if self.min_lower_case_chars { + write!(f, " PASSWORD_MIN_LOWER_CASE_CHARS")?; + } + if self.min_numeric_chars { + write!(f, " PASSWORD_MIN_NUMERIC_CHARS")?; + } + if self.min_special_chars { + write!(f, " PASSWORD_MIN_SPECIAL_CHARS")?; + } + if self.min_age_days { + write!(f, " PASSWORD_MIN_AGE_DAYS")?; + } + if self.max_age_days { + write!(f, " PASSWORD_MAX_AGE_DAYS")?; + } + if self.max_retries { + write!(f, " PASSWORD_MAX_RETRIES")?; + } + if self.lockout_time_mins { + write!(f, " PASSWORD_LOCKOUT_TIME_MINS")?; + } + if self.history { + write!(f, " PASSWORD_HISTORY")?; + } + if self.comment { + write!(f, " COMMENT")?; + } + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DropPasswordPolicyStmt { + pub if_exists: bool, + pub name: String, +} + +impl Display for DropPasswordPolicyStmt { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "DROP PASSWORD POLICY ")?; + if self.if_exists { + write!(f, "IF EXISTS ")?; + } + write!(f, "{}", self.name)?; + + Ok(()) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct DescPasswordPolicyStmt { + pub name: String, +} + +impl Display for DescPasswordPolicyStmt { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + write!(f, "DESCRIBE PASSWORD POLICY {}", self.name)?; + + Ok(()) + } +} diff --git a/src/query/ast/src/ast/statements/statement.rs b/src/query/ast/src/ast/statements/statement.rs index ac3e1b3cf5e14..97f12bcf83f07 100644 --- a/src/query/ast/src/ast/statements/statement.rs +++ b/src/query/ast/src/ast/statements/statement.rs @@ -253,6 +253,15 @@ pub enum Statement { DescNetworkPolicy(DescNetworkPolicyStmt), ShowNetworkPolicies, + // password policy + CreatePasswordPolicy(CreatePasswordPolicyStmt), + AlterPasswordPolicy(AlterPasswordPolicyStmt), + DropPasswordPolicy(DropPasswordPolicyStmt), + DescPasswordPolicy(DescPasswordPolicyStmt), + ShowPasswordPolicies { + show_options: Option<ShowOptions>, + }, + // tasks CreateTask(CreateTaskStmt), AlterTask(AlterTaskStmt), @@ -590,6 +599,16 @@ impl Display for Statement { Statement::DropNetworkPolicy(stmt) => write!(f, "{stmt}")?, Statement::DescNetworkPolicy(stmt) => write!(f, "{stmt}")?, Statement::ShowNetworkPolicies => write!(f, "SHOW NETWORK POLICIES")?, + Statement::CreatePasswordPolicy(stmt) => write!(f, "{stmt}")?, + Statement::AlterPasswordPolicy(stmt) => write!(f, "{stmt}")?, + Statement::DropPasswordPolicy(stmt) => write!(f, "{stmt}")?, + Statement::DescPasswordPolicy(stmt) => write!(f, "{stmt}")?, + Statement::ShowPasswordPolicies { show_options } => { + write!(f, "SHOW PASSWORD POLICIES")?; + if let Some(show_options) = show_options { + write!(f, " {show_options}")?; + } + } Statement::CreateTask(stmt) => write!(f, "{stmt}")?, Statement::AlterTask(stmt) => write!(f, "{stmt}")?, Statement::ExecuteTask(stmt) => write!(f, "{stmt}")?, diff --git a/src/query/ast/src/ast/statements/user.rs b/src/query/ast/src/ast/statements/user.rs index bacd63f294dbf..b4d75a3d7b6f4 100644 --- a/src/query/ast/src/ast/statements/user.rs +++ b/src/query/ast/src/ast/statements/user.rs @@ -168,6 +168,8 @@ pub enum UserOptionItem { DefaultRole(String), SetNetworkPolicy(String), UnsetNetworkPolicy, + SetPasswordPolicy(String), + UnsetPasswordPolicy, } impl UserOptionItem { @@ -179,6 +181,8 @@ impl UserOptionItem { Self::DefaultRole(v) => option.set_default_role(Some(v.clone())), Self::SetNetworkPolicy(v) => option.set_network_policy(Some(v.clone())), Self::UnsetNetworkPolicy => option.set_network_policy(None), + Self::SetPasswordPolicy(v) => option.set_password_policy(Some(v.clone())), + Self::UnsetPasswordPolicy => option.set_password_policy(None), } } } @@ -247,6 +251,8 @@ impl Display for UserOptionItem { UserOptionItem::DefaultRole(v) => write!(f, "DEFAULT_ROLE = '{}'", v), UserOptionItem::SetNetworkPolicy(v) => write!(f, "SET NETWORK POLICY = '{}'", v), UserOptionItem::UnsetNetworkPolicy => write!(f, "UNSET NETWORK POLICY"), + UserOptionItem::SetPasswordPolicy(v) => write!(f, "SET PASSWORD POLICY = '{}'", v), + UserOptionItem::UnsetPasswordPolicy => write!(f, "UNSET PASSWORD POLICY"), } } } diff --git a/src/query/ast/src/parser/statement.rs b/src/query/ast/src/parser/statement.rs index 745219128de9b..d5b440b00ca93 100644 --- a/src/query/ast/src/parser/statement.rs +++ b/src/query/ast/src/parser/statement.rs @@ -1521,10 +1521,10 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { let create_network_policy = map( rule! { - CREATE ~ NETWORK ~ POLICY ~ ( IF ~ ^NOT ~ ^EXISTS )? ~ #ident - ~ ALLOWED_IP_LIST ~ Eq ~ "(" ~ ^#comma_separated_list0(literal_string) ~ ")" - ~ ( BLOCKED_IP_LIST ~ Eq ~ "(" ~ ^#comma_separated_list0(literal_string) ~ ")" ) ? - ~ ( COMMENT ~ Eq ~ #literal_string)? + CREATE ~ NETWORK ~ ^POLICY ~ ( IF ~ ^NOT ~ ^EXISTS )? ~ ^#ident + ~ ALLOWED_IP_LIST ~ ^Eq ~ ^"(" ~ ^#comma_separated_list0(literal_string) ~ ^")" + ~ ( BLOCKED_IP_LIST ~ ^Eq ~ ^"(" ~ ^#comma_separated_list0(literal_string) ~ ^")" ) ? + ~ ( COMMENT ~ ^Eq ~ ^#literal_string)? }, |( _, @@ -1558,10 +1558,10 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { ); let alter_network_policy = map( rule! { - ALTER ~ NETWORK ~ POLICY ~ ( IF ~ ^EXISTS )? ~ #ident ~ SET - ~ ( ALLOWED_IP_LIST ~ Eq ~ "(" ~ ^#comma_separated_list0(literal_string) ~ ")" ) ? - ~ ( BLOCKED_IP_LIST ~ Eq ~ "(" ~ ^#comma_separated_list0(literal_string) ~ ")" ) ? - ~ ( COMMENT ~ Eq ~ #literal_string)? + ALTER ~ NETWORK ~ ^POLICY ~ ( IF ~ ^EXISTS )? ~ ^#ident ~ SET + ~ ( ALLOWED_IP_LIST ~ ^Eq ~ ^"(" ~ ^#comma_separated_list0(literal_string) ~ ^")" ) ? + ~ ( BLOCKED_IP_LIST ~ ^Eq ~ ^"(" ~ ^#comma_separated_list0(literal_string) ~ ^")" ) ? + ~ ( COMMENT ~ ^Eq ~ ^#literal_string)? }, |( _, @@ -1595,7 +1595,7 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { ); let drop_network_policy = map( rule! { - DROP ~ NETWORK ~ POLICY ~ ( IF ~ ^EXISTS )? ~ #ident + DROP ~ NETWORK ~ ^POLICY ~ ( IF ~ ^EXISTS )? ~ ^#ident }, |(_, _, _, opt_if_exists, name)| { let stmt = DropNetworkPolicyStmt { @@ -1607,7 +1607,7 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { ); let describe_network_policy = map( rule! { - ( DESC | DESCRIBE ) ~ NETWORK ~ POLICY ~ #ident + ( DESC | DESCRIBE ) ~ NETWORK ~ ^POLICY ~ ^#ident }, |(_, _, _, name)| { Statement::DescNetworkPolicy(DescNetworkPolicyStmt { @@ -1617,7 +1617,64 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { ); let show_network_policies = value( Statement::ShowNetworkPolicies, - rule! { SHOW ~ NETWORK ~ POLICIES }, + rule! { SHOW ~ NETWORK ~ ^POLICIES }, + ); + + let create_password_policy = map( + rule! { + CREATE ~ PASSWORD ~ ^POLICY ~ ( IF ~ ^NOT ~ ^EXISTS )? ~ ^#ident + ~ #password_set_options + }, + |(_, _, _, opt_if_not_exists, name, set_options)| { + let stmt = CreatePasswordPolicyStmt { + if_not_exists: opt_if_not_exists.is_some(), + name: name.to_string(), + set_options, + }; + Statement::CreatePasswordPolicy(stmt) + }, + ); + let alter_password_policy = map( + rule! { + ALTER ~ PASSWORD ~ ^POLICY ~ ( IF ~ ^EXISTS )? ~ ^#ident + ~ #alter_password_action + }, + |(_, _, _, opt_if_exists, name, action)| { + let stmt = AlterPasswordPolicyStmt { + if_exists: opt_if_exists.is_some(), + name: name.to_string(), + action, + }; + Statement::AlterPasswordPolicy(stmt) + }, + ); + let drop_password_policy = map( + rule! { + DROP ~ PASSWORD ~ ^POLICY ~ ( IF ~ ^EXISTS )? ~ ^#ident + }, + |(_, _, _, opt_if_exists, name)| { + let stmt = DropPasswordPolicyStmt { + if_exists: opt_if_exists.is_some(), + name: name.to_string(), + }; + Statement::DropPasswordPolicy(stmt) + }, + ); + let describe_password_policy = map( + rule! { + ( DESC | DESCRIBE ) ~ PASSWORD ~ ^POLICY ~ ^#ident + }, + |(_, _, _, name)| { + Statement::DescPasswordPolicy(DescPasswordPolicyStmt { + name: name.to_string(), + }) + }, + ); + let show_password_policies = map( + rule! { + SHOW ~ PASSWORD ~ ^POLICIES ~ ^#show_options? + }, + |(_, _, _, show_options)| Statement::ShowPasswordPolicies { show_options }, ); let create_pipe = map( @@ -1707,13 +1764,18 @@ pub fn statement(i: Input) -> IResult<StatementWithFormat> { | #alter_database : "`ALTER DATABASE [IF EXISTS] <action>`" | #use_database : "`USE <database>`" ), - // network policy + // network policy / password policy rule!( #create_network_policy: "`CREATE NETWORK POLICY [IF NOT EXISTS] name ALLOWED_IP_LIST = ('ip1' [, 'ip2']) [BLOCKED_IP_LIST = ('ip1' [, 'ip2'])] [COMMENT = '<string_literal>']`" | #alter_network_policy: "`ALTER NETWORK POLICY [IF EXISTS] name SET [ALLOWED_IP_LIST = ('ip1' [, 'ip2'])] [BLOCKED_IP_LIST = ('ip1' [, 'ip2'])] [COMMENT = '<string_literal>']`" | #drop_network_policy: "`DROP NETWORK POLICY [IF EXISTS] name`" | #describe_network_policy: "`DESC NETWORK POLICY name`" | #show_network_policies: "`SHOW NETWORK POLICIES`" + | #create_password_policy: "`CREATE PASSWORD POLICY [IF NOT EXISTS] name [PASSWORD_MIN_LENGTH = <u64_literal>] ... [COMMENT = '<string_literal>']`" + | #alter_password_policy: "`ALTER PASSWORD POLICY [IF EXISTS] name SET [PASSWORD_MIN_LENGTH = <u64_literal>] ... [COMMENT = '<string_literal>']`" + | #drop_password_policy: "`DROP PASSWORD POLICY [IF EXISTS] name`" + | #describe_password_policy: "`DESC PASSWORD POLICY name`" + | #show_password_policies: "`SHOW PASSWORD POLICIES [<show_options>]`" ), rule!( #insert : "`INSERT INTO [TABLE] <table> [(<column>, ...)] (FORMAT <format> | VALUES <values> | <query>)`" @@ -2907,14 +2969,16 @@ pub fn limit_where(i: Input) -> IResult<ShowLimit> { )(i) } -pub fn show_limit(i: Input) -> IResult<ShowLimit> { - let limit_like = map( +pub fn limit_like(i: Input) -> IResult<ShowLimit> { + map( rule! { LIKE ~ #literal_string }, |(_, pattern)| ShowLimit::Like { pattern }, - ); + )(i) +} +pub fn show_limit(i: Input) -> IResult<ShowLimit> { rule!( #limit_like | #limit_where @@ -3030,6 +3094,11 @@ pub fn catalog_type(i: Input) -> IResult<CatalogType> { } pub fn user_option(i: Input) -> IResult<UserOptionItem> { + let tenant_setting = value(UserOptionItem::TenantSetting(true), rule! { TENANTSETTING }); + let no_tenant_setting = value( + UserOptionItem::TenantSetting(false), + rule! { NOTENANTSETTING }, + ); let default_role_option = map( rule! { "DEFAULT_ROLE" ~ ^"=" ~ ^#role_name @@ -3038,26 +3107,38 @@ pub fn user_option(i: Input) -> IResult<UserOptionItem> { ); let set_network_policy = map( rule! { - SET ~ ^NETWORK ~ ^POLICY ~ ^"=" ~ ^#literal_string + SET ~ NETWORK ~ ^POLICY ~ ^"=" ~ ^#literal_string }, |(_, _, _, _, policy)| UserOptionItem::SetNetworkPolicy(policy), ); let unset_network_policy = map( rule! { - UNSET ~ ^NETWORK ~ ^POLICY + UNSET ~ NETWORK ~ ^POLICY }, |(_, _, _)| UserOptionItem::UnsetNetworkPolicy, ); - alt(( - value(UserOptionItem::TenantSetting(true), rule! { TENANTSETTING }), - value( - UserOptionItem::TenantSetting(false), - rule! { NOTENANTSETTING }, - ), - default_role_option, - set_network_policy, - unset_network_policy, - ))(i) + let set_password_policy = map( + rule! { + SET ~ PASSWORD ~ ^POLICY ~ ^"=" ~ ^#literal_string + }, + |(_, _, _, _, policy)| UserOptionItem::SetPasswordPolicy(policy), + ); + let unset_password_policy = map( + rule! { + UNSET ~ PASSWORD ~ ^POLICY + }, + |(_, _, _)| UserOptionItem::UnsetPasswordPolicy, + ); + + rule!( + #tenant_setting + | #no_tenant_setting + | #default_role_option + | #set_network_policy + | #unset_network_policy + | #set_password_policy + | #unset_password_policy + )(i) } pub fn user_identity(i: Input) -> IResult<UserIdentity> { @@ -3213,3 +3294,119 @@ pub fn merge_update_expr(i: Input) -> IResult<MergeUpdateExpr> { |((table, name), _, expr)| MergeUpdateExpr { table, name, expr }, )(i) } + +pub fn password_set_options(i: Input) -> IResult<PasswordSetOptions> { + map( + rule! { + ( PASSWORD_MIN_LENGTH ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MAX_LENGTH ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MIN_UPPER_CASE_CHARS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MIN_LOWER_CASE_CHARS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MIN_NUMERIC_CHARS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MIN_SPECIAL_CHARS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MIN_AGE_DAYS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MAX_AGE_DAYS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_MAX_RETRIES ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_LOCKOUT_TIME_MINS ~ Eq ~ ^#literal_u64 ) ? + ~ ( PASSWORD_HISTORY ~ Eq ~ ^#literal_u64 ) ? + ~ ( COMMENT ~ Eq ~ ^#literal_string)? + }, + |( + opt_min_length, + opt_max_length, + opt_min_upper_case_chars, + opt_min_lower_case_chars, + opt_min_numeric_chars, + opt_min_special_chars, + opt_min_age_days, + opt_max_age_days, + opt_max_retries, + opt_lockout_time_mins, + opt_history, + opt_comment, + )| { + PasswordSetOptions { + min_length: opt_min_length.map(|opt| opt.2), + max_length: opt_max_length.map(|opt| opt.2), + min_upper_case_chars: opt_min_upper_case_chars.map(|opt| opt.2), + min_lower_case_chars: opt_min_lower_case_chars.map(|opt| opt.2), + min_numeric_chars: opt_min_numeric_chars.map(|opt| opt.2), + min_special_chars: opt_min_special_chars.map(|opt| opt.2), + min_age_days: opt_min_age_days.map(|opt| opt.2), + max_age_days: opt_max_age_days.map(|opt| opt.2), + max_retries: opt_max_retries.map(|opt| opt.2), + lockout_time_mins: opt_lockout_time_mins.map(|opt| opt.2), + history: opt_history.map(|opt| opt.2), + comment: opt_comment.map(|opt| opt.2), + } + }, + )(i) +} + +pub fn password_unset_options(i: Input) -> IResult<PasswordUnSetOptions> { + map( + rule! { + PASSWORD_MIN_LENGTH ? + ~ PASSWORD_MAX_LENGTH ? + ~ PASSWORD_MIN_UPPER_CASE_CHARS ? + ~ PASSWORD_MIN_LOWER_CASE_CHARS ? + ~ PASSWORD_MIN_NUMERIC_CHARS ? + ~ PASSWORD_MIN_SPECIAL_CHARS ? + ~ PASSWORD_MIN_AGE_DAYS ? + ~ PASSWORD_MAX_AGE_DAYS ? + ~ PASSWORD_MAX_RETRIES ? + ~ PASSWORD_LOCKOUT_TIME_MINS ? + ~ PASSWORD_HISTORY ? + ~ COMMENT ? + }, + |( + opt_min_length, + opt_max_length, + opt_min_upper_case_chars, + opt_min_lower_case_chars, + opt_min_numeric_chars, + opt_min_special_chars, + opt_min_age_days, + opt_max_age_days, + opt_max_retries, + opt_lockout_time_mins, + opt_history, + opt_comment, + )| { + PasswordUnSetOptions { + min_length: opt_min_length.is_some(), + max_length: opt_max_length.is_some(), + min_upper_case_chars: opt_min_upper_case_chars.is_some(), + min_lower_case_chars: opt_min_lower_case_chars.is_some(), + min_numeric_chars: opt_min_numeric_chars.is_some(), + min_special_chars: opt_min_special_chars.is_some(), + min_age_days: opt_min_age_days.is_some(), + max_age_days: opt_max_age_days.is_some(), + max_retries: opt_max_retries.is_some(), + lockout_time_mins: opt_lockout_time_mins.is_some(), + history: opt_history.is_some(), + comment: opt_comment.is_some(), + } + }, + )(i) +} + +pub fn alter_password_action(i: Input) -> IResult<AlterPasswordAction> { + let set_options = map( + rule! { + SET ~ #password_set_options + }, + |(_, set_options)| AlterPasswordAction::SetOptions(set_options), + ); + let unset_options = map( + rule! { + UNSET ~ #password_unset_options + }, + |(_, unset_options)| AlterPasswordAction::UnSetOptions(unset_options), + ); + + rule!( + #set_options + | #unset_options + )(i) +} diff --git a/src/query/ast/src/parser/token.rs b/src/query/ast/src/parser/token.rs index 867dc9f3a1207..c7f7f48934f9f 100644 --- a/src/query/ast/src/parser/token.rs +++ b/src/query/ast/src/parser/token.rs @@ -747,6 +747,30 @@ pub enum TokenKind { PARTITION, #[token("PARQUET", ignore(ascii_case))] PARQUET, + #[token("PASSWORD", ignore(ascii_case))] + PASSWORD, + #[token("PASSWORD_MIN_LENGTH", ignore(ascii_case))] + PASSWORD_MIN_LENGTH, + #[token("PASSWORD_MAX_LENGTH", ignore(ascii_case))] + PASSWORD_MAX_LENGTH, + #[token("PASSWORD_MIN_UPPER_CASE_CHARS", ignore(ascii_case))] + PASSWORD_MIN_UPPER_CASE_CHARS, + #[token("PASSWORD_MIN_LOWER_CASE_CHARS", ignore(ascii_case))] + PASSWORD_MIN_LOWER_CASE_CHARS, + #[token("PASSWORD_MIN_NUMERIC_CHARS", ignore(ascii_case))] + PASSWORD_MIN_NUMERIC_CHARS, + #[token("PASSWORD_MIN_SPECIAL_CHARS", ignore(ascii_case))] + PASSWORD_MIN_SPECIAL_CHARS, + #[token("PASSWORD_MIN_AGE_DAYS", ignore(ascii_case))] + PASSWORD_MIN_AGE_DAYS, + #[token("PASSWORD_MAX_AGE_DAYS", ignore(ascii_case))] + PASSWORD_MAX_AGE_DAYS, + #[token("PASSWORD_MAX_RETRIES", ignore(ascii_case))] + PASSWORD_MAX_RETRIES, + #[token("PASSWORD_LOCKOUT_TIME_MINS", ignore(ascii_case))] + PASSWORD_LOCKOUT_TIME_MINS, + #[token("PASSWORD_HISTORY", ignore(ascii_case))] + PASSWORD_HISTORY, #[token("PATTERN", ignore(ascii_case))] PATTERN, #[token("PIPELINE", ignore(ascii_case))] diff --git a/src/query/ast/src/visitors/visitor.rs b/src/query/ast/src/visitors/visitor.rs index c264243f81137..ec77a3328da96 100644 --- a/src/query/ast/src/visitors/visitor.rs +++ b/src/query/ast/src/visitors/visitor.rs @@ -672,6 +672,16 @@ pub trait Visitor<'ast>: Sized { fn visit_show_network_policies(&mut self) {} + fn visit_create_password_policy(&mut self, _stmt: &'ast CreatePasswordPolicyStmt) {} + + fn visit_alter_password_policy(&mut self, _stmt: &'ast AlterPasswordPolicyStmt) {} + + fn visit_drop_password_policy(&mut self, _stmt: &'ast DropPasswordPolicyStmt) {} + + fn visit_desc_password_policy(&mut self, _stmt: &'ast DescPasswordPolicyStmt) {} + + fn visit_show_password_policies(&mut self, _show_options: &'ast Option<ShowOptions>) {} + fn visit_create_task(&mut self, _stmt: &'ast CreateTaskStmt) {} fn visit_drop_task(&mut self, _stmt: &'ast DropTaskStmt) {} diff --git a/src/query/ast/src/visitors/visitor_mut.rs b/src/query/ast/src/visitors/visitor_mut.rs index fa1cf82bc2370..114ab2962ebd3 100644 --- a/src/query/ast/src/visitors/visitor_mut.rs +++ b/src/query/ast/src/visitors/visitor_mut.rs @@ -686,6 +686,16 @@ pub trait VisitorMut: Sized { fn visit_show_network_policies(&mut self) {} + fn visit_create_password_policy(&mut self, _stmt: &mut CreatePasswordPolicyStmt) {} + + fn visit_alter_password_policy(&mut self, _stmt: &mut AlterPasswordPolicyStmt) {} + + fn visit_drop_password_policy(&mut self, _stmt: &mut DropPasswordPolicyStmt) {} + + fn visit_desc_password_policy(&mut self, _stmt: &mut DescPasswordPolicyStmt) {} + + fn visit_show_password_policies(&mut self, _show_options: &mut Option<ShowOptions>) {} + fn visit_create_task(&mut self, _stmt: &mut CreateTaskStmt) {} fn visit_drop_task(&mut self, _stmt: &mut DropTaskStmt) {} diff --git a/src/query/ast/src/visitors/walk.rs b/src/query/ast/src/visitors/walk.rs index 13813a2d3ea7b..72c28634b2377 100644 --- a/src/query/ast/src/visitors/walk.rs +++ b/src/query/ast/src/visitors/walk.rs @@ -504,6 +504,13 @@ pub fn walk_statement<'a, V: Visitor<'a>>(visitor: &mut V, statement: &'a Statem Statement::DropNetworkPolicy(stmt) => visitor.visit_drop_network_policy(stmt), Statement::DescNetworkPolicy(stmt) => visitor.visit_desc_network_policy(stmt), Statement::ShowNetworkPolicies => visitor.visit_show_network_policies(), + Statement::CreatePasswordPolicy(stmt) => visitor.visit_create_password_policy(stmt), + Statement::AlterPasswordPolicy(stmt) => visitor.visit_alter_password_policy(stmt), + Statement::DropPasswordPolicy(stmt) => visitor.visit_drop_password_policy(stmt), + Statement::DescPasswordPolicy(stmt) => visitor.visit_desc_password_policy(stmt), + Statement::ShowPasswordPolicies { show_options } => { + visitor.visit_show_password_policies(show_options) + } Statement::CreateTask(stmt) => visitor.visit_create_task(stmt), Statement::ExecuteTask(stmt) => visitor.visit_execute_task(stmt), Statement::DropTask(stmt) => visitor.visit_drop_task(stmt), diff --git a/src/query/ast/src/visitors/walk_mut.rs b/src/query/ast/src/visitors/walk_mut.rs index 990da8deebb59..03ef1bd14b4a4 100644 --- a/src/query/ast/src/visitors/walk_mut.rs +++ b/src/query/ast/src/visitors/walk_mut.rs @@ -509,6 +509,13 @@ pub fn walk_statement_mut<V: VisitorMut>(visitor: &mut V, statement: &mut Statem Statement::DropNetworkPolicy(stmt) => visitor.visit_drop_network_policy(stmt), Statement::DescNetworkPolicy(stmt) => visitor.visit_desc_network_policy(stmt), Statement::ShowNetworkPolicies => visitor.visit_show_network_policies(), + Statement::CreatePasswordPolicy(stmt) => visitor.visit_create_password_policy(stmt), + Statement::AlterPasswordPolicy(stmt) => visitor.visit_alter_password_policy(stmt), + Statement::DropPasswordPolicy(stmt) => visitor.visit_drop_password_policy(stmt), + Statement::DescPasswordPolicy(stmt) => visitor.visit_desc_password_policy(stmt), + Statement::ShowPasswordPolicies { show_options } => { + visitor.visit_show_password_policies(show_options) + } Statement::CreateTask(stmt) => visitor.visit_create_task(stmt), Statement::ExecuteTask(stmt) => visitor.visit_execute_task(stmt), diff --git a/src/query/ast/tests/it/testdata/statement-error.txt b/src/query/ast/tests/it/testdata/statement-error.txt index 5236c275c3c9c..4b94a3eda84b7 100644 --- a/src/query/ast/tests/it/testdata/statement-error.txt +++ b/src/query/ast/tests/it/testdata/statement-error.txt @@ -131,7 +131,7 @@ error: --> SQL:1:6 | 1 | drop a - | ^ unexpected `a`, expecting `TASK`, `TABLE`, `MASKING`, `CATALOG`, `DATABASE`, `AGGREGATING`, `SCHEMA`, `NETWORK`, `VIEW`, `STREAM`, `VIRTUAL`, `USER`, `ROLE`, `FUNCTION`, `STAGE`, `FILE`, `SHARE`, `PIPE`, or `CONNECTION` + | ^ unexpected `a`, expecting `TASK`, `TABLE`, `MASKING`, `CATALOG`, `DATABASE`, `PASSWORD`, `AGGREGATING`, `SCHEMA`, `NETWORK`, `VIEW`, `STREAM`, `VIRTUAL`, `USER`, `ROLE`, `FUNCTION`, `STAGE`, `FILE`, `SHARE`, `PIPE`, or `CONNECTION` ---------- Input ---------- @@ -197,7 +197,7 @@ error: --> SQL:1:6 | 1 | drop usar if exists 'test-j'; - | ^^^^ unexpected `usar`, expecting `USER`, `SHARE`, `STREAM`, `STAGE`, `AGGREGATING`, `ROLE`, `TABLE`, `SCHEMA`, `NETWORK`, `VIRTUAL`, `CATALOG`, `DATABASE`, `FUNCTION`, `TASK`, `MASKING`, `VIEW`, `FILE`, `PIPE`, or `CONNECTION` + | ^^^^ unexpected `usar`, expecting `USER`, `SHARE`, `STREAM`, `STAGE`, `PASSWORD`, `AGGREGATING`, `ROLE`, `TABLE`, `SCHEMA`, `NETWORK`, `VIRTUAL`, `CATALOG`, `DATABASE`, `FUNCTION`, `TASK`, `MASKING`, `VIEW`, `FILE`, `PIPE`, or `CONNECTION` ---------- Input ---------- @@ -237,7 +237,7 @@ error: --> SQL:1:6 | 1 | SHOW GRANT FOR ROLE 'role1'; - | ^^^^^ unexpected `GRANT`, expecting `GRANTS`, `CREATE`, `NETWORK`, `VIRTUAL`, `STREAMS`, `CATALOGS`, `FUNCTIONS`, `DATABASES`, `CONNECTIONS`, `TABLE_FUNCTIONS`, `DROP`, `TABLE`, `ROLES`, `SHARE`, `TASKS`, `INDEXES`, `COLUMNS`, `PROCESSLIST`, `STAGES`, `TABLES`, `SHARES`, `ENGINES`, `METRICS`, `SETTINGS`, `LOCKS`, `SCHEMAS`, `FIELDS`, `USERS`, `FILE`, or `FULL` + | ^^^^^ unexpected `GRANT`, expecting `GRANTS`, `CREATE`, `NETWORK`, `VIRTUAL`, `STREAMS`, `CATALOGS`, `FUNCTIONS`, `DATABASES`, `CONNECTIONS`, `TABLE_FUNCTIONS`, `DROP`, `TABLE`, `ROLES`, `SHARE`, `TASKS`, `INDEXES`, `COLUMNS`, `PASSWORD`, `PROCESSLIST`, `STAGES`, `TABLES`, `SHARES`, `ENGINES`, `METRICS`, `SETTINGS`, `LOCKS`, `SCHEMAS`, `FIELDS`, `USERS`, `FILE`, or `FULL` ---------- Input ---------- diff --git a/src/query/management/src/lib.rs b/src/query/management/src/lib.rs index 35c959bb8b9a5..c70a693fe09bb 100644 --- a/src/query/management/src/lib.rs +++ b/src/query/management/src/lib.rs @@ -18,6 +18,7 @@ mod cluster; mod connection; mod file_format; mod network_policy; +mod password_policy; mod quota; mod role; mod serde; @@ -34,6 +35,8 @@ pub use file_format::FileFormatApi; pub use file_format::FileFormatMgr; pub use network_policy::NetworkPolicyApi; pub use network_policy::NetworkPolicyMgr; +pub use password_policy::PasswordPolicyApi; +pub use password_policy::PasswordPolicyMgr; pub use quota::QuotaApi; pub use quota::QuotaMgr; pub use role::RoleApi; diff --git a/src/query/management/src/password_policy/mod.rs b/src/query/management/src/password_policy/mod.rs new file mode 100644 index 0000000000000..33bab1ac7e0be --- /dev/null +++ b/src/query/management/src/password_policy/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod password_policy_api; +mod password_policy_mgr; + +pub use password_policy_api::PasswordPolicyApi; +pub use password_policy_mgr::PasswordPolicyMgr; diff --git a/src/query/management/src/password_policy/password_policy_api.rs b/src/query/management/src/password_policy/password_policy_api.rs new file mode 100644 index 0000000000000..6217cf8eb9718 --- /dev/null +++ b/src/query/management/src/password_policy/password_policy_api.rs @@ -0,0 +1,35 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use databend_common_exception::Result; +use databend_common_meta_app::principal::PasswordPolicy; +use databend_common_meta_types::MatchSeq; +use databend_common_meta_types::SeqV; + +#[async_trait::async_trait] +pub trait PasswordPolicyApi: Sync + Send { + async fn add_password_policy(&self, password_policy: PasswordPolicy) -> Result<u64>; + + async fn update_password_policy( + &self, + password_policy: PasswordPolicy, + seq: MatchSeq, + ) -> Result<u64>; + + async fn drop_password_policy(&self, name: &str, seq: MatchSeq) -> Result<()>; + + async fn get_password_policy(&self, name: &str, seq: MatchSeq) -> Result<SeqV<PasswordPolicy>>; + + async fn get_password_policies(&self) -> Result<Vec<PasswordPolicy>>; +} diff --git a/src/query/management/src/password_policy/password_policy_mgr.rs b/src/query/management/src/password_policy/password_policy_mgr.rs new file mode 100644 index 0000000000000..d7ed580595d3e --- /dev/null +++ b/src/query/management/src/password_policy/password_policy_mgr.rs @@ -0,0 +1,175 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use databend_common_base::base::escape_for_key; +use databend_common_exception::ErrorCode; +use databend_common_exception::Result; +use databend_common_meta_app::principal::PasswordPolicy; +use databend_common_meta_kvapi::kvapi; +use databend_common_meta_kvapi::kvapi::UpsertKVReq; +use databend_common_meta_types::MatchSeq; +use databend_common_meta_types::MatchSeqExt; +use databend_common_meta_types::MetaError; +use databend_common_meta_types::Operation; +use databend_common_meta_types::SeqV; + +use crate::password_policy::password_policy_api::PasswordPolicyApi; +use crate::serde::deserialize_struct; +use crate::serde::serialize_struct; + +static PASSWORD_POLICY_API_KEY_PREFIX: &str = "__fd_password_policies"; + +pub struct PasswordPolicyMgr { + kv_api: Arc<dyn kvapi::KVApi<Error = MetaError>>, + password_policy_prefix: String, +} + +impl PasswordPolicyMgr { + pub fn create( + kv_api: Arc<dyn kvapi::KVApi<Error = MetaError>>, + tenant: &str, + ) -> Result<Self, ErrorCode> { + if tenant.is_empty() { + return Err(ErrorCode::TenantIsEmpty( + "Tenant can not empty (while create password policy)", + )); + } + + Ok(PasswordPolicyMgr { + kv_api, + password_policy_prefix: format!("{}/{}", PASSWORD_POLICY_API_KEY_PREFIX, tenant), + }) + } + + fn make_password_policy_key(&self, name: &str) -> Result<String> { + Ok(format!( + "{}/{}", + self.password_policy_prefix, + escape_for_key(name)? + )) + } +} + +#[async_trait::async_trait] +impl PasswordPolicyApi for PasswordPolicyMgr { + #[async_backtrace::framed] + #[minitrace::trace] + async fn add_password_policy(&self, password_policy: PasswordPolicy) -> Result<u64> { + let match_seq = MatchSeq::Exact(0); + let key = self.make_password_policy_key(password_policy.name.as_str())?; + let value = Operation::Update(serialize_struct( + &password_policy, + ErrorCode::IllegalPasswordPolicy, + || "", + )?); + + let kv_api = self.kv_api.clone(); + let upsert_kv = kv_api.upsert_kv(UpsertKVReq::new(&key, match_seq, value, None)); + + let res_seq = upsert_kv.await?.added_seq_or_else(|v| { + ErrorCode::PasswordPolicyAlreadyExists(format!( + "PasswordPolicy already exists, seq [{}]", + v.seq + )) + })?; + + Ok(res_seq) + } + + #[async_backtrace::framed] + #[minitrace::trace] + async fn update_password_policy( + &self, + password_policy: PasswordPolicy, + match_seq: MatchSeq, + ) -> Result<u64> { + let key = self.make_password_policy_key(password_policy.name.as_str())?; + let value = Operation::Update(serialize_struct( + &password_policy, + ErrorCode::IllegalPasswordPolicy, + || "", + )?); + + let kv_api = self.kv_api.clone(); + let upsert_kv = kv_api + .upsert_kv(UpsertKVReq::new(&key, match_seq, value, None)) + .await?; + + match upsert_kv.result { + Some(SeqV { seq: s, .. }) => Ok(s), + None => Err(ErrorCode::UnknownPasswordPolicy(format!( + "Unknown PasswordPolicy, or seq not match {}", + password_policy.name.clone() + ))), + } + } + + #[async_backtrace::framed] + #[minitrace::trace] + async fn drop_password_policy(&self, name: &str, seq: MatchSeq) -> Result<()> { + let key = self.make_password_policy_key(name)?; + let kv_api = self.kv_api.clone(); + let res = kv_api + .upsert_kv(UpsertKVReq::new(&key, seq, Operation::Delete, None)) + .await?; + if res.prev.is_some() && res.result.is_none() { + Ok(()) + } else { + Err(ErrorCode::UnknownPasswordPolicy(format!( + "Unknown PasswordPolicy {}", + name + ))) + } + } + + #[async_backtrace::framed] + #[minitrace::trace] + async fn get_password_policy(&self, name: &str, seq: MatchSeq) -> Result<SeqV<PasswordPolicy>> { + let key = self.make_password_policy_key(name)?; + let res = self.kv_api.get_kv(&key).await?; + let seq_value = res.ok_or_else(|| { + ErrorCode::UnknownPasswordPolicy(format!("Unknown PasswordPolicy {}", name)) + })?; + + match seq.match_seq(&seq_value) { + Ok(_) => Ok(SeqV::new( + seq_value.seq, + deserialize_struct(&seq_value.data, ErrorCode::IllegalPasswordPolicy, || "")?, + )), + Err(_) => Err(ErrorCode::UnknownPasswordPolicy(format!( + "Unknown PasswordPolicy {}", + name + ))), + } + } + + #[async_backtrace::framed] + #[minitrace::trace] + async fn get_password_policies(&self) -> Result<Vec<PasswordPolicy>> { + let values = self + .kv_api + .prefix_list_kv(&self.password_policy_prefix) + .await?; + + let mut password_policies = Vec::with_capacity(values.len()); + for (_, value) in values { + let password_policy = + deserialize_struct(&value.data, ErrorCode::IllegalPasswordPolicy, || "")?; + password_policies.push(password_policy); + } + Ok(password_policies) + } +} diff --git a/src/query/service/src/databases/system/system_database.rs b/src/query/service/src/databases/system/system_database.rs index 9fd11b1e7c456..328b118121b1d 100644 --- a/src/query/service/src/databases/system/system_database.rs +++ b/src/query/service/src/databases/system/system_database.rs @@ -41,6 +41,7 @@ use databend_common_storages_system::MallocStatsTable; use databend_common_storages_system::MallocStatsTotalsTable; use databend_common_storages_system::MetricsTable; use databend_common_storages_system::OneTable; +use databend_common_storages_system::PasswordPoliciesTable; use databend_common_storages_system::ProcessesTable; use databend_common_storages_system::ProcessorProfileTable; use databend_common_storages_system::QueryCacheTable; @@ -127,6 +128,7 @@ impl SystemDatabase { ProcessorProfileTable::create(sys_db_meta.next_table_id()), LocksTable::create(sys_db_meta.next_table_id()), VirtualColumnsTable::create(sys_db_meta.next_table_id()), + PasswordPoliciesTable::create(sys_db_meta.next_table_id()), ]; let disable_tables = Self::disable_system_tables(); diff --git a/src/query/service/src/interpreters/access/management_mode_access.rs b/src/query/service/src/interpreters/access/management_mode_access.rs index 0bab8c57edec5..f871d4c579bc9 100644 --- a/src/query/service/src/interpreters/access/management_mode_access.rs +++ b/src/query/service/src/interpreters/access/management_mode_access.rs @@ -104,6 +104,10 @@ impl AccessChecker for ManagementModeAccess { | Plan::CreateNetworkPolicy(_) | Plan::AlterNetworkPolicy(_) | Plan::DropNetworkPolicy(_) + // Password policy. + | Plan::CreatePasswordPolicy(_) + | Plan::AlterPasswordPolicy(_) + | Plan::DropPasswordPolicy(_) // UDF | Plan::CreateUDF(_) diff --git a/src/query/service/src/interpreters/access/privilege_access.rs b/src/query/service/src/interpreters/access/privilege_access.rs index ff547435adc71..e1357c38bce71 100644 --- a/src/query/service/src/interpreters/access/privilege_access.rs +++ b/src/query/service/src/interpreters/access/privilege_access.rs @@ -932,6 +932,10 @@ impl AccessChecker for PrivilegeAccess { | Plan::DropNetworkPolicy(_) | Plan::DescNetworkPolicy(_) | Plan::ShowNetworkPolicies(_) + | Plan::CreatePasswordPolicy(_) + | Plan::AlterPasswordPolicy(_) + | Plan::DropPasswordPolicy(_) + | Plan::DescPasswordPolicy(_) | Plan::CreateConnection(_) | Plan::ShowConnections(_) | Plan::DescConnection(_) diff --git a/src/query/service/src/interpreters/interpreter_factory.rs b/src/query/service/src/interpreters/interpreter_factory.rs index 4e394dc38924c..f373276f51d65 100644 --- a/src/query/service/src/interpreters/interpreter_factory.rs +++ b/src/query/service/src/interpreters/interpreter_factory.rs @@ -471,6 +471,20 @@ impl InterpreterFactory { Plan::ShowNetworkPolicies(_) => { Ok(Arc::new(ShowNetworkPoliciesInterpreter::try_create(ctx)?)) } + Plan::CreatePasswordPolicy(p) => Ok(Arc::new( + CreatePasswordPolicyInterpreter::try_create(ctx, *p.clone())?, + )), + Plan::AlterPasswordPolicy(p) => Ok(Arc::new( + AlterPasswordPolicyInterpreter::try_create(ctx, *p.clone())?, + )), + Plan::DropPasswordPolicy(p) => Ok(Arc::new(DropPasswordPolicyInterpreter::try_create( + ctx, + *p.clone(), + )?)), + Plan::DescPasswordPolicy(p) => Ok(Arc::new(DescPasswordPolicyInterpreter::try_create( + ctx, + *p.clone(), + )?)), Plan::CreateTask(p) => Ok(Arc::new(CreateTaskInterpreter::try_create( ctx, diff --git a/src/query/service/src/interpreters/interpreter_password_policy_alter.rs b/src/query/service/src/interpreters/interpreter_password_policy_alter.rs new file mode 100644 index 0000000000000..54037769241fd --- /dev/null +++ b/src/query/service/src/interpreters/interpreter_password_policy_alter.rs @@ -0,0 +1,172 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use databend_common_ast::ast::AlterPasswordAction; +use databend_common_exception::Result; +use databend_common_sql::plans::AlterPasswordPolicyPlan; +use databend_common_users::UserApiProvider; +use databend_common_users::DEFAULT_PASSWORD_HISTORY; +use databend_common_users::DEFAULT_PASSWORD_LOCKOUT_TIME_MINS; +use databend_common_users::DEFAULT_PASSWORD_MAX_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MAX_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MAX_RETRIES; +use databend_common_users::DEFAULT_PASSWORD_MIN_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MIN_CHARS; +use databend_common_users::DEFAULT_PASSWORD_MIN_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MIN_SPECIAL_CHARS; +use log::debug; + +use crate::interpreters::Interpreter; +use crate::pipelines::PipelineBuildResult; +use crate::sessions::QueryContext; +use crate::sessions::TableContext; + +#[derive(Debug)] +pub struct AlterPasswordPolicyInterpreter { + ctx: Arc<QueryContext>, + plan: AlterPasswordPolicyPlan, +} + +impl AlterPasswordPolicyInterpreter { + pub fn try_create(ctx: Arc<QueryContext>, plan: AlterPasswordPolicyPlan) -> Result<Self> { + Ok(AlterPasswordPolicyInterpreter { ctx, plan }) + } +} + +#[async_trait::async_trait] +impl Interpreter for AlterPasswordPolicyInterpreter { + fn name(&self) -> &str { + "AlterPasswordPolicyInterpreter" + } + + #[minitrace::trace] + #[async_backtrace::framed] + async fn execute2(&self) -> Result<PipelineBuildResult> { + debug!("ctx.id" = self.ctx.get_id().as_str(); "alter_password_policy_execute"); + + let plan = self.plan.clone(); + let tenant = self.ctx.get_tenant(); + let user_mgr = UserApiProvider::instance(); + + match plan.action { + AlterPasswordAction::SetOptions(set_options) => { + user_mgr + .update_password_policy( + &tenant, + &plan.name, + set_options.min_length, + set_options.max_length, + set_options.min_upper_case_chars, + set_options.min_lower_case_chars, + set_options.min_numeric_chars, + set_options.min_special_chars, + set_options.min_age_days, + set_options.max_age_days, + set_options.max_retries, + set_options.lockout_time_mins, + set_options.history, + set_options.comment.clone(), + plan.if_exists, + ) + .await?; + } + AlterPasswordAction::UnSetOptions(unset_options) => { + // convert unset options to default values + let min_length = if unset_options.min_length { + Some(DEFAULT_PASSWORD_MIN_LENGTH) + } else { + None + }; + let max_length = if unset_options.max_length { + Some(DEFAULT_PASSWORD_MAX_LENGTH) + } else { + None + }; + let min_upper_case_chars = if unset_options.min_upper_case_chars { + Some(DEFAULT_PASSWORD_MIN_CHARS) + } else { + None + }; + let min_lower_case_chars = if unset_options.min_lower_case_chars { + Some(DEFAULT_PASSWORD_MIN_CHARS) + } else { + None + }; + let min_numeric_chars = if unset_options.min_numeric_chars { + Some(DEFAULT_PASSWORD_MIN_CHARS) + } else { + None + }; + let min_special_chars = if unset_options.min_special_chars { + Some(DEFAULT_PASSWORD_MIN_SPECIAL_CHARS) + } else { + None + }; + let min_age_days = if unset_options.min_age_days { + Some(DEFAULT_PASSWORD_MIN_AGE_DAYS) + } else { + None + }; + let max_age_days = if unset_options.max_age_days { + Some(DEFAULT_PASSWORD_MAX_AGE_DAYS) + } else { + None + }; + let max_retries = if unset_options.max_retries { + Some(DEFAULT_PASSWORD_MAX_RETRIES) + } else { + None + }; + let lockout_time_mins = if unset_options.lockout_time_mins { + Some(DEFAULT_PASSWORD_LOCKOUT_TIME_MINS) + } else { + None + }; + let history = if unset_options.history { + Some(DEFAULT_PASSWORD_HISTORY) + } else { + None + }; + let comment = if unset_options.comment { + Some("".to_string()) + } else { + None + }; + user_mgr + .update_password_policy( + &tenant, + &plan.name, + min_length, + max_length, + min_upper_case_chars, + min_lower_case_chars, + min_numeric_chars, + min_special_chars, + min_age_days, + max_age_days, + max_retries, + lockout_time_mins, + history, + comment, + plan.if_exists, + ) + .await?; + } + } + + Ok(PipelineBuildResult::create()) + } +} diff --git a/src/query/service/src/interpreters/interpreter_password_policy_create.rs b/src/query/service/src/interpreters/interpreter_password_policy_create.rs new file mode 100644 index 0000000000000..8cee486852824 --- /dev/null +++ b/src/query/service/src/interpreters/interpreter_password_policy_create.rs @@ -0,0 +1,132 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use chrono::Utc; +use databend_common_exception::Result; +use databend_common_meta_app::principal::PasswordPolicy; +use databend_common_sql::plans::CreatePasswordPolicyPlan; +use databend_common_users::UserApiProvider; +use databend_common_users::DEFAULT_PASSWORD_HISTORY; +use databend_common_users::DEFAULT_PASSWORD_LOCKOUT_TIME_MINS; +use databend_common_users::DEFAULT_PASSWORD_MAX_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MAX_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MAX_RETRIES; +use databend_common_users::DEFAULT_PASSWORD_MIN_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MIN_CHARS; +use databend_common_users::DEFAULT_PASSWORD_MIN_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MIN_SPECIAL_CHARS; +use log::debug; + +use crate::interpreters::Interpreter; +use crate::pipelines::PipelineBuildResult; +use crate::sessions::QueryContext; +use crate::sessions::TableContext; + +#[derive(Debug)] +pub struct CreatePasswordPolicyInterpreter { + ctx: Arc<QueryContext>, + plan: CreatePasswordPolicyPlan, +} + +impl CreatePasswordPolicyInterpreter { + pub fn try_create(ctx: Arc<QueryContext>, plan: CreatePasswordPolicyPlan) -> Result<Self> { + Ok(CreatePasswordPolicyInterpreter { ctx, plan }) + } +} + +#[async_trait::async_trait] +impl Interpreter for CreatePasswordPolicyInterpreter { + fn name(&self) -> &str { + "CreatePasswordPolicyInterpreter" + } + + #[minitrace::trace] + #[async_backtrace::framed] + async fn execute2(&self) -> Result<PipelineBuildResult> { + debug!("ctx.id" = self.ctx.get_id().as_str(); "create_password_policy_execute"); + + let plan = self.plan.clone(); + let tenant = self.ctx.get_tenant(); + let user_mgr = UserApiProvider::instance(); + + let min_length = plan + .set_options + .min_length + .unwrap_or(DEFAULT_PASSWORD_MIN_LENGTH); + let max_length = plan + .set_options + .max_length + .unwrap_or(DEFAULT_PASSWORD_MAX_LENGTH); + let min_upper_case_chars = plan + .set_options + .min_upper_case_chars + .unwrap_or(DEFAULT_PASSWORD_MIN_CHARS); + let min_lower_case_chars = plan + .set_options + .min_lower_case_chars + .unwrap_or(DEFAULT_PASSWORD_MIN_CHARS); + let min_numeric_chars = plan + .set_options + .min_numeric_chars + .unwrap_or(DEFAULT_PASSWORD_MIN_CHARS); + let min_special_chars = plan + .set_options + .min_special_chars + .unwrap_or(DEFAULT_PASSWORD_MIN_SPECIAL_CHARS); + let min_age_days = plan + .set_options + .min_age_days + .unwrap_or(DEFAULT_PASSWORD_MIN_AGE_DAYS); + let max_age_days = plan + .set_options + .max_age_days + .unwrap_or(DEFAULT_PASSWORD_MAX_AGE_DAYS); + let max_retries = plan + .set_options + .max_retries + .unwrap_or(DEFAULT_PASSWORD_MAX_RETRIES); + let lockout_time_mins = plan + .set_options + .lockout_time_mins + .unwrap_or(DEFAULT_PASSWORD_LOCKOUT_TIME_MINS); + let history = plan.set_options.history.unwrap_or(DEFAULT_PASSWORD_HISTORY); + + let comment = plan.set_options.comment.clone().unwrap_or_default(); + + let password_policy = PasswordPolicy { + name: plan.name, + min_length, + max_length, + min_upper_case_chars, + min_lower_case_chars, + min_numeric_chars, + min_special_chars, + min_age_days, + max_age_days, + max_retries, + lockout_time_mins, + history, + comment, + create_on: Utc::now(), + update_on: None, + }; + user_mgr + .add_password_policy(&tenant, password_policy, plan.if_not_exists) + .await?; + + Ok(PipelineBuildResult::create()) + } +} diff --git a/src/query/service/src/interpreters/interpreter_password_policy_desc.rs b/src/query/service/src/interpreters/interpreter_password_policy_desc.rs new file mode 100644 index 0000000000000..8aafd73e6aeb7 --- /dev/null +++ b/src/query/service/src/interpreters/interpreter_password_policy_desc.rs @@ -0,0 +1,149 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use databend_common_exception::Result; +use databend_common_expression::types::StringType; +use databend_common_expression::types::UInt64Type; +use databend_common_expression::DataBlock; +use databend_common_expression::FromData; +use databend_common_sql::plans::DescPasswordPolicyPlan; +use databend_common_users::UserApiProvider; +use databend_common_users::DEFAULT_PASSWORD_HISTORY; +use databend_common_users::DEFAULT_PASSWORD_LOCKOUT_TIME_MINS; +use databend_common_users::DEFAULT_PASSWORD_MAX_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MAX_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MAX_RETRIES; +use databend_common_users::DEFAULT_PASSWORD_MIN_AGE_DAYS; +use databend_common_users::DEFAULT_PASSWORD_MIN_CHARS; +use databend_common_users::DEFAULT_PASSWORD_MIN_LENGTH; +use databend_common_users::DEFAULT_PASSWORD_MIN_SPECIAL_CHARS; + +use crate::interpreters::Interpreter; +use crate::pipelines::PipelineBuildResult; +use crate::sessions::QueryContext; +use crate::sessions::TableContext; + +#[derive(Debug)] +pub struct DescPasswordPolicyInterpreter { + ctx: Arc<QueryContext>, + plan: DescPasswordPolicyPlan, +} + +impl DescPasswordPolicyInterpreter { + pub fn try_create(ctx: Arc<QueryContext>, plan: DescPasswordPolicyPlan) -> Result<Self> { + Ok(DescPasswordPolicyInterpreter { ctx, plan }) + } +} + +#[async_trait::async_trait] +impl Interpreter for DescPasswordPolicyInterpreter { + fn name(&self) -> &str { + "DescPasswordPolicyInterpreter" + } + + #[async_backtrace::framed] + async fn execute2(&self) -> Result<PipelineBuildResult> { + let tenant = self.ctx.get_tenant(); + let user_mgr = UserApiProvider::instance(); + + let password_policy = user_mgr + .get_password_policy(&tenant, self.plan.name.as_str()) + .await?; + + let properties = vec![ + "NAME".as_bytes().to_vec(), + "COMMENT".as_bytes().to_vec(), + "PASSWORD_MIN_LENGTH".as_bytes().to_vec(), + "PASSWORD_MAX_LENGTH".as_bytes().to_vec(), + "PASSWORD_MIN_UPPER_CASE_CHARS".as_bytes().to_vec(), + "PASSWORD_MIN_LOWER_CASE_CHARS".as_bytes().to_vec(), + "PASSWORD_MIN_NUMERIC_CHARS".as_bytes().to_vec(), + "PASSWORD_MIN_SPECIAL_CHARS".as_bytes().to_vec(), + "PASSWORD_MIN_AGE_DAYS".as_bytes().to_vec(), + "PASSWORD_MAX_AGE_DAYS".as_bytes().to_vec(), + "PASSWORD_MAX_RETRIES".as_bytes().to_vec(), + "PASSWORD_LOCKOUT_TIME_MINS".as_bytes().to_vec(), + "PASSWORD_HISTORY".as_bytes().to_vec(), + ]; + + let min_length = format!("{}", password_policy.min_length); + let max_length = format!("{}", password_policy.max_length); + let min_upper_case_chars = format!("{}", password_policy.min_upper_case_chars); + let min_lower_case_chars = format!("{}", password_policy.min_lower_case_chars); + let min_numeric_chars = format!("{}", password_policy.min_numeric_chars); + let min_special_chars = format!("{}", password_policy.min_special_chars); + let min_age_days = format!("{}", password_policy.min_age_days); + let max_age_days = format!("{}", password_policy.max_age_days); + let max_retries = format!("{}", password_policy.max_retries); + let lockout_time_mins = format!("{}", password_policy.lockout_time_mins); + let history = format!("{}", password_policy.history); + + let values = vec![ + password_policy.name.as_bytes().to_vec(), + password_policy.comment.as_bytes().to_vec(), + min_length.as_bytes().to_vec(), + max_length.as_bytes().to_vec(), + min_upper_case_chars.as_bytes().to_vec(), + min_lower_case_chars.as_bytes().to_vec(), + min_numeric_chars.as_bytes().to_vec(), + min_special_chars.as_bytes().to_vec(), + min_age_days.as_bytes().to_vec(), + max_age_days.as_bytes().to_vec(), + max_retries.as_bytes().to_vec(), + lockout_time_mins.as_bytes().to_vec(), + history.as_bytes().to_vec(), + ]; + + let defaults = vec![ + None, + None, + Some(DEFAULT_PASSWORD_MIN_LENGTH), + Some(DEFAULT_PASSWORD_MAX_LENGTH), + Some(DEFAULT_PASSWORD_MIN_CHARS), + Some(DEFAULT_PASSWORD_MIN_CHARS), + Some(DEFAULT_PASSWORD_MIN_CHARS), + Some(DEFAULT_PASSWORD_MIN_SPECIAL_CHARS), + Some(DEFAULT_PASSWORD_MIN_AGE_DAYS), + Some(DEFAULT_PASSWORD_MAX_AGE_DAYS), + Some(DEFAULT_PASSWORD_MAX_RETRIES), + Some(DEFAULT_PASSWORD_LOCKOUT_TIME_MINS), + Some(DEFAULT_PASSWORD_HISTORY), + ]; + + let descriptions = vec![ + "Name of password policy.".as_bytes().to_vec(), + "Comment of password policy.".as_bytes().to_vec(), + "Minimum length of new password.".as_bytes().to_vec(), + "Maximum length of new password.".as_bytes().to_vec(), + "Minimum number of uppercase characters in new password.".as_bytes().to_vec(), + "Minimum number of lowercase characters in new password.".as_bytes().to_vec(), + "Minimum number of numeric characters in new password.".as_bytes().to_vec(), + "Minimum number of special characters in new password.".as_bytes().to_vec(), + "Period after a password is changed during which a password cannot be changed again, in days.".as_bytes().to_vec(), + "Period after which password must be changed, in days.".as_bytes().to_vec(), + "Number of attempts users have to enter the correct password before their account is locked.".as_bytes().to_vec(), + "Period of time for which users will be locked after entering their password incorrectly many times (specified by MAX_RETRIES), in minutes.".as_bytes().to_vec(), + "Number of most recent passwords that may not be repeated by the user.".as_bytes().to_vec(), + ]; + + PipelineBuildResult::from_blocks(vec![DataBlock::new_from_columns(vec![ + StringType::from_data(properties), + StringType::from_data(values), + UInt64Type::from_opt_data(defaults), + StringType::from_data(descriptions), + ])]) + } +} diff --git a/src/query/service/src/interpreters/interpreter_password_policy_drop.rs b/src/query/service/src/interpreters/interpreter_password_policy_drop.rs new file mode 100644 index 0000000000000..df4f868f105cd --- /dev/null +++ b/src/query/service/src/interpreters/interpreter_password_policy_drop.rs @@ -0,0 +1,60 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use databend_common_exception::Result; +use databend_common_sql::plans::DropPasswordPolicyPlan; +use databend_common_users::UserApiProvider; +use log::debug; + +use crate::interpreters::Interpreter; +use crate::pipelines::PipelineBuildResult; +use crate::sessions::QueryContext; +use crate::sessions::TableContext; + +#[derive(Debug)] +pub struct DropPasswordPolicyInterpreter { + ctx: Arc<QueryContext>, + plan: DropPasswordPolicyPlan, +} + +impl DropPasswordPolicyInterpreter { + pub fn try_create(ctx: Arc<QueryContext>, plan: DropPasswordPolicyPlan) -> Result<Self> { + Ok(DropPasswordPolicyInterpreter { ctx, plan }) + } +} + +#[async_trait::async_trait] +impl Interpreter for DropPasswordPolicyInterpreter { + fn name(&self) -> &str { + "DropPasswordPolicyInterpreter" + } + + #[minitrace::trace] + #[async_backtrace::framed] + async fn execute2(&self) -> Result<PipelineBuildResult> { + debug!("ctx.id" = self.ctx.get_id().as_str(); "drop_password_policy_execute"); + + let plan = self.plan.clone(); + let tenant = self.ctx.get_tenant(); + + let user_mgr = UserApiProvider::instance(); + user_mgr + .drop_password_policy(&tenant, plan.name.as_str(), plan.if_exists) + .await?; + + Ok(PipelineBuildResult::create()) + } +} diff --git a/src/query/service/src/interpreters/mod.rs b/src/query/service/src/interpreters/mod.rs index bbe152bdbfd84..35cf8d28d5c3b 100644 --- a/src/query/service/src/interpreters/mod.rs +++ b/src/query/service/src/interpreters/mod.rs @@ -55,6 +55,10 @@ mod interpreter_network_policy_alter; mod interpreter_network_policy_create; mod interpreter_network_policy_desc; mod interpreter_network_policy_drop; +mod interpreter_password_policy_alter; +mod interpreter_password_policy_create; +mod interpreter_password_policy_desc; +mod interpreter_password_policy_drop; mod interpreter_presign; mod interpreter_privilege_grant; mod interpreter_privilege_revoke; @@ -155,6 +159,10 @@ pub use interpreter_network_policy_alter::AlterNetworkPolicyInterpreter; pub use interpreter_network_policy_create::CreateNetworkPolicyInterpreter; pub use interpreter_network_policy_desc::DescNetworkPolicyInterpreter; pub use interpreter_network_policy_drop::DropNetworkPolicyInterpreter; +pub use interpreter_password_policy_alter::AlterPasswordPolicyInterpreter; +pub use interpreter_password_policy_create::CreatePasswordPolicyInterpreter; +pub use interpreter_password_policy_desc::DescPasswordPolicyInterpreter; +pub use interpreter_password_policy_drop::DropPasswordPolicyInterpreter; pub use interpreter_privilege_grant::GrantPrivilegeInterpreter; pub use interpreter_privilege_revoke::RevokePrivilegeInterpreter; pub use interpreter_replace::ReplaceInterpreter; diff --git a/src/query/service/tests/it/storages/testdata/columns_table.txt b/src/query/service/tests/it/storages/testdata/columns_table.txt index f4c61bf33535b..4c2bda0dc14b2 100644 --- a/src/query/service/tests/it/storages/testdata/columns_table.txt +++ b/src/query/service/tests/it/storages/testdata/columns_table.txt @@ -49,6 +49,7 @@ DB.Table: 'system'.'columns', Table: columns-table_id:1, ver:0, Engine: SystemCo | 'command' | 'system' | 'processes' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'comment' | 'information_schema' | 'statistics' | 'NULL' | 'NULL' | '' | '' | 'NO' | '' | | 'comment' | 'system' | 'columns' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | +| 'comment' | 'system' | 'password_policies' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'comment' | 'system' | 'stages' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'comment' | 'system' | 'streams' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'comment' | 'system' | 'task_history' | 'Nullable(String)' | 'VARCHAR' | '' | '' | 'YES' | '' | @@ -66,6 +67,7 @@ DB.Table: 'system'.'columns', Table: columns-table_id:1, ver:0, Engine: SystemCo | 'created_on' | 'system' | 'background_tasks' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'created_on' | 'system' | 'indexes' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'created_on' | 'system' | 'locks' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | +| 'created_on' | 'system' | 'password_policies' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'created_on' | 'system' | 'stages' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'created_on' | 'system' | 'streams' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'created_on' | 'system' | 'tables' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | @@ -214,6 +216,7 @@ DB.Table: 'system'.'columns', Table: columns-table_id:1, ver:0, Engine: SystemCo | 'name' | 'system' | 'functions' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'name' | 'system' | 'indexes' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'name' | 'system' | 'malloc_stats_totals' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | +| 'name' | 'system' | 'password_policies' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'name' | 'system' | 'roles' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'name' | 'system' | 'settings' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'name' | 'system' | 'stages' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | @@ -253,6 +256,7 @@ DB.Table: 'system'.'columns', Table: columns-table_id:1, ver:0, Engine: SystemCo | 'operator_id' | 'system' | 'query_profile' | 'UInt32' | 'INT UNSIGNED' | '' | '' | 'NO' | '' | | 'operator_id' | 'system' | 'query_summary' | 'UInt32' | 'INT UNSIGNED' | '' | '' | 'NO' | '' | | 'operator_type' | 'system' | 'query_summary' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | +| 'options' | 'system' | 'password_policies' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'ordinal_position' | 'information_schema' | 'columns' | 'UInt8' | 'TINYINT UNSIGNED' | '' | '' | 'NO' | '' | | 'ordinal_position' | 'information_schema' | 'key_column_usage' | 'NULL' | 'NULL' | '' | '' | 'NO' | '' | | 'owner' | 'system' | 'databases' | 'Nullable(String)' | 'VARCHAR' | '' | '' | 'YES' | '' | @@ -381,6 +385,7 @@ DB.Table: 'system'.'columns', Table: columns-table_id:1, ver:0, Engine: SystemCo | 'type' | 'system' | 'settings' | 'String' | 'VARCHAR' | '' | '' | 'NO' | '' | | 'updated_on' | 'system' | 'background_tasks' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'updated_on' | 'system' | 'indexes' | 'Nullable(Timestamp)' | 'TIMESTAMP' | '' | '' | 'YES' | '' | +| 'updated_on' | 'system' | 'password_policies' | 'Nullable(Timestamp)' | 'TIMESTAMP' | '' | '' | 'YES' | '' | | 'updated_on' | 'system' | 'streams' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'updated_on' | 'system' | 'tables' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | | 'updated_on' | 'system' | 'tables_with_history' | 'Timestamp' | 'TIMESTAMP' | '' | '' | 'NO' | '' | diff --git a/src/query/sql/Cargo.toml b/src/query/sql/Cargo.toml index 398039106d162..e58ce6b204df6 100644 --- a/src/query/sql/Cargo.toml +++ b/src/query/sql/Cargo.toml @@ -74,6 +74,7 @@ num-traits = "0.2.15" opendal = { workspace = true } ordered-float = { workspace = true } parking_lot = { workspace = true } +passwords = { version = "3.1.16", features = ["common-password"] } percent-encoding = "2" regex = { workspace = true } roaring = "0.10.1" diff --git a/src/query/sql/src/planner/binder/binder.rs b/src/query/sql/src/planner/binder/binder.rs index 166cfd8e2d45b..6dae835b03e93 100644 --- a/src/query/sql/src/planner/binder/binder.rs +++ b/src/query/sql/src/planner/binder/binder.rs @@ -557,6 +557,19 @@ impl<'a> Binder { Statement::ShowNetworkPolicies => { self.bind_show_network_policies().await? } + Statement::CreatePasswordPolicy(stmt) => { + self.bind_create_password_policy(stmt).await? + } + Statement::AlterPasswordPolicy(stmt) => { + self.bind_alter_password_policy(stmt).await? + } + Statement::DropPasswordPolicy(stmt) => { + self.bind_drop_password_policy(stmt).await? + } + Statement::DescPasswordPolicy(stmt) => { + self.bind_desc_password_policy(stmt).await? + } + Statement::ShowPasswordPolicies{ show_options } => self.bind_show_password_policies(bind_context, show_options).await?, Statement::CreateTask(stmt) => { self.bind_create_task(stmt).await? } diff --git a/src/query/sql/src/planner/binder/ddl/account.rs b/src/query/sql/src/planner/binder/ddl/account.rs index 25df651327c31..1e6f3a7ba0f06 100644 --- a/src/query/sql/src/planner/binder/ddl/account.rs +++ b/src/query/sql/src/planner/binder/ddl/account.rs @@ -15,15 +15,18 @@ use databend_common_ast::ast::AccountMgrLevel; use databend_common_ast::ast::AccountMgrSource; use databend_common_ast::ast::AlterUserStmt; +use databend_common_ast::ast::AuthOption; use databend_common_ast::ast::CreateUserStmt; use databend_common_ast::ast::GrantStmt; use databend_common_ast::ast::RevokeStmt; +use databend_common_exception::ErrorCode; use databend_common_exception::Result; use databend_common_meta_app::principal::AuthInfo; use databend_common_meta_app::principal::GrantObject; use databend_common_meta_app::principal::UserOption; use databend_common_meta_app::principal::UserPrivilegeSet; use databend_common_users::UserApiProvider; +use passwords::analyzer; use crate::plans::AlterUserPlan; use crate::plans::CreateUserPlan; @@ -161,6 +164,8 @@ impl Binder { for option in user_options { option.apply(&mut user_option); } + self.verify_password(&user_option, auth_option).await?; + let plan = CreateUserPlan { user: user.clone(), auth_info: AuthInfo::create2(&auth_option.auth_type, &auth_option.password)?, @@ -189,8 +194,15 @@ impl Binder { .await? }; + let mut user_option = user_info.option.clone(); + for option in user_options { + option.apply(&mut user_option); + } + // None means no change to make let new_auth_info = if let Some(auth_option) = &auth_option { + // verify the password if changed + self.verify_password(&user_option, auth_option).await?; let auth_info = user_info .auth_info .alter2(&auth_option.auth_type, &auth_option.password)?; @@ -203,10 +215,6 @@ impl Binder { None }; - let mut user_option = user_info.option.clone(); - for option in user_options { - option.apply(&mut user_option); - } let new_user_option = if user_option == user_info.option { None } else { @@ -220,4 +228,73 @@ impl Binder { Ok(Plan::AlterUser(Box::new(plan))) } + + // Verify the password according to the options of the password policy + #[async_backtrace::framed] + async fn verify_password( + &mut self, + user_option: &UserOption, + auth_option: &AuthOption, + ) -> Result<()> { + if let (Some(name), Some(password)) = (user_option.password_policy(), &auth_option.password) + { + if let Ok(password_policy) = UserApiProvider::instance() + .get_password_policy(&self.ctx.get_tenant(), name) + .await + { + let analyzed = analyzer::analyze(password); + + let mut invalids = Vec::new(); + if analyzed.length() < password_policy.min_length as usize + || analyzed.length() > password_policy.max_length as usize + { + invalids.push(format!( + "expect length range {} to {}, but got {}", + password_policy.min_length, + password_policy.max_length, + analyzed.length() + )); + } + if analyzed.uppercase_letters_count() + < password_policy.min_upper_case_chars as usize + { + invalids.push(format!( + "expect {} uppercase chars, but got {}", + password_policy.min_upper_case_chars, + analyzed.uppercase_letters_count() + )); + } + if analyzed.lowercase_letters_count() + < password_policy.min_lower_case_chars as usize + { + invalids.push(format!( + "expect {} lowercase chars, but got {}", + password_policy.min_lower_case_chars, + analyzed.lowercase_letters_count() + )); + } + if analyzed.numbers_count() < password_policy.min_numeric_chars as usize { + invalids.push(format!( + "expect {} numeric chars, but got {}", + password_policy.min_numeric_chars, + analyzed.numbers_count() + )); + } + if analyzed.symbols_count() < password_policy.min_special_chars as usize { + invalids.push(format!( + "expect {} special chars, but got {}", + password_policy.min_special_chars, + analyzed.symbols_count() + )); + } + if !invalids.is_empty() { + return Err(ErrorCode::InvalidPassword(format!( + "Invalid password: {}", + invalids.join(", ") + ))); + } + } + } + Ok(()) + } } diff --git a/src/query/sql/src/planner/binder/ddl/mod.rs b/src/query/sql/src/planner/binder/ddl/mod.rs index f20c2c98c2399..b38e29f239691 100644 --- a/src/query/sql/src/planner/binder/ddl/mod.rs +++ b/src/query/sql/src/planner/binder/ddl/mod.rs @@ -20,6 +20,7 @@ mod data_mask; mod database; mod index; mod network_policy; +mod password_policy; mod role; mod share; mod stage; diff --git a/src/query/sql/src/planner/binder/ddl/password_policy.rs b/src/query/sql/src/planner/binder/ddl/password_policy.rs new file mode 100644 index 0000000000000..4ea58e3f63fb3 --- /dev/null +++ b/src/query/sql/src/planner/binder/ddl/password_policy.rs @@ -0,0 +1,115 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use databend_common_ast::ast::*; +use databend_common_exception::Result; + +use crate::binder::show::get_show_options; +use crate::binder::Binder; +use crate::plans::AlterPasswordPolicyPlan; +use crate::plans::CreatePasswordPolicyPlan; +use crate::plans::DescPasswordPolicyPlan; +use crate::plans::DropPasswordPolicyPlan; +use crate::plans::Plan; +use crate::plans::RewriteKind; +use crate::BindContext; + +impl Binder { + #[async_backtrace::framed] + pub(in crate::planner::binder) async fn bind_create_password_policy( + &mut self, + stmt: &CreatePasswordPolicyStmt, + ) -> Result<Plan> { + let CreatePasswordPolicyStmt { + if_not_exists, + name, + set_options, + } = stmt; + + let tenant = self.ctx.get_tenant(); + let plan = CreatePasswordPolicyPlan { + if_not_exists: *if_not_exists, + tenant, + name: name.to_string(), + set_options: set_options.clone(), + }; + Ok(Plan::CreatePasswordPolicy(Box::new(plan))) + } + + #[async_backtrace::framed] + pub(in crate::planner::binder) async fn bind_alter_password_policy( + &mut self, + stmt: &AlterPasswordPolicyStmt, + ) -> Result<Plan> { + let AlterPasswordPolicyStmt { + if_exists, + name, + action, + } = stmt; + + let tenant = self.ctx.get_tenant(); + let plan = AlterPasswordPolicyPlan { + if_exists: *if_exists, + tenant, + name: name.to_string(), + action: action.clone(), + }; + Ok(Plan::AlterPasswordPolicy(Box::new(plan))) + } + + #[async_backtrace::framed] + pub(in crate::planner::binder) async fn bind_drop_password_policy( + &mut self, + stmt: &DropPasswordPolicyStmt, + ) -> Result<Plan> { + let DropPasswordPolicyStmt { if_exists, name } = stmt; + + let tenant = self.ctx.get_tenant(); + let plan = DropPasswordPolicyPlan { + if_exists: *if_exists, + tenant, + name: name.to_string(), + }; + Ok(Plan::DropPasswordPolicy(Box::new(plan))) + } + + #[async_backtrace::framed] + pub(in crate::planner::binder) async fn bind_desc_password_policy( + &mut self, + stmt: &DescPasswordPolicyStmt, + ) -> Result<Plan> { + let DescPasswordPolicyStmt { name } = stmt; + + let plan = DescPasswordPolicyPlan { + name: name.to_string(), + }; + Ok(Plan::DescPasswordPolicy(Box::new(plan))) + } + + #[async_backtrace::framed] + pub(in crate::planner::binder) async fn bind_show_password_policies( + &mut self, + bind_context: &mut BindContext, + show_options: &Option<ShowOptions>, + ) -> Result<Plan> { + let (show_limit, limit_str) = get_show_options(show_options, None); + let query = format!( + "SELECT name, comment, options FROM system.password_policies {} order by name {}", + show_limit, limit_str, + ); + + self.bind_rewrite_to_query(bind_context, &query, RewriteKind::ShowPasswordPolicies) + .await + } +} diff --git a/src/query/sql/src/planner/binder/show.rs b/src/query/sql/src/planner/binder/show.rs index 6c1b2e12a7480..b2fcd17505ae2 100644 --- a/src/query/sql/src/planner/binder/show.rs +++ b/src/query/sql/src/planner/binder/show.rs @@ -134,7 +134,7 @@ impl Binder { show_limit, limit_str, ); - self.bind_rewrite_to_query(bind_context, &query, RewriteKind::ShowProcessList) + self.bind_rewrite_to_query(bind_context, &query, RewriteKind::ShowIndexes) .await } @@ -167,17 +167,21 @@ impl Binder { } } -fn get_show_options(show_options: &Option<ShowOptions>, col: Option<String>) -> (String, String) { +pub(crate) fn get_show_options( + show_options: &Option<ShowOptions>, + col: Option<String>, +) -> (String, String) { let mut show_limit = String::new(); let mut limit_str = String::new(); if let Some(show_option) = show_options { match &show_option.show_limit { Some(ShowLimit::Like { pattern }) => { + // convert like pattern to lowercase to uses case-insensitive pattern matching if let Some(col) = &col { - show_limit = format!("WHERE {} LIKE '{}'", col, pattern); + show_limit = format!("WHERE LOWER({}) LIKE '{}'", col, pattern.to_lowercase()); } else { - show_limit = format!("WHERE name LIKE '{}'", pattern); + show_limit = format!("WHERE LOWER(name) LIKE '{}'", pattern.to_lowercase()); } } Some(ShowLimit::Where { selection }) => { diff --git a/src/query/sql/src/planner/format/display_plan.rs b/src/query/sql/src/planner/format/display_plan.rs index 4618bc929f846..516ee0492de60 100644 --- a/src/query/sql/src/planner/format/display_plan.rs +++ b/src/query/sql/src/planner/format/display_plan.rs @@ -177,6 +177,12 @@ impl Plan { Plan::DescNetworkPolicy(_) => Ok("DescNetworkPolicy".to_string()), Plan::ShowNetworkPolicies(_) => Ok("ShowNetworkPolicies".to_string()), + // password policy + Plan::CreatePasswordPolicy(_) => Ok("CreatePasswordPolicy".to_string()), + Plan::AlterPasswordPolicy(_) => Ok("AlterPasswordPolicy".to_string()), + Plan::DropPasswordPolicy(_) => Ok("DropPasswordPolicy".to_string()), + Plan::DescPasswordPolicy(_) => Ok("DescPasswordPolicy".to_string()), + // task Plan::CreateTask(_) => Ok("CreateTask".to_string()), Plan::DropTask(_) => Ok("DropTask".to_string()), diff --git a/src/query/sql/src/planner/plans/ddl/account.rs b/src/query/sql/src/planner/plans/ddl/account.rs index 76c54d7404d64..2f0d280f4c64b 100644 --- a/src/query/sql/src/planner/plans/ddl/account.rs +++ b/src/query/sql/src/planner/plans/ddl/account.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use databend_common_ast::ast::AlterPasswordAction; +use databend_common_ast::ast::PasswordSetOptions; use databend_common_expression::types::DataType; use databend_common_expression::types::NumberDataType; use databend_common_expression::DataField; @@ -195,3 +197,60 @@ impl ShowNetworkPoliciesPlan { ]) } } + +#[derive(Clone, Debug, PartialEq)] +pub struct CreatePasswordPolicyPlan { + pub if_not_exists: bool, + pub tenant: String, + pub name: String, + pub set_options: PasswordSetOptions, +} + +impl CreatePasswordPolicyPlan { + pub fn schema(&self) -> DataSchemaRef { + DataSchemaRefExt::create(vec![]) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct AlterPasswordPolicyPlan { + pub if_exists: bool, + pub tenant: String, + pub name: String, + pub action: AlterPasswordAction, +} + +impl AlterPasswordPolicyPlan { + pub fn schema(&self) -> DataSchemaRef { + DataSchemaRefExt::create(vec![]) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct DropPasswordPolicyPlan { + pub if_exists: bool, + pub tenant: String, + pub name: String, +} + +impl DropPasswordPolicyPlan { + pub fn schema(&self) -> DataSchemaRef { + DataSchemaRefExt::create(vec![]) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct DescPasswordPolicyPlan { + pub name: String, +} + +impl DescPasswordPolicyPlan { + pub fn schema(&self) -> DataSchemaRef { + DataSchemaRefExt::create(vec![ + DataField::new("Property", DataType::String), + DataField::new("Value", DataType::String), + DataField::new("Default", DataType::Nullable(Box::new(DataType::String))), + DataField::new("Description", DataType::String), + ]) + } +} diff --git a/src/query/sql/src/planner/plans/plan.rs b/src/query/sql/src/planner/plans/plan.rs index 7f5366a53ee7a..01b54814def9b 100644 --- a/src/query/sql/src/planner/plans/plan.rs +++ b/src/query/sql/src/planner/plans/plan.rs @@ -29,6 +29,7 @@ use crate::optimizer::SExpr; use crate::plans::copy_into_location::CopyIntoLocationPlan; use crate::plans::AddTableColumnPlan; use crate::plans::AlterNetworkPolicyPlan; +use crate::plans::AlterPasswordPolicyPlan; use crate::plans::AlterShareTenantsPlan; use crate::plans::AlterTableClusterKeyPlan; use crate::plans::AlterTaskPlan; @@ -46,6 +47,7 @@ use crate::plans::CreateDatamaskPolicyPlan; use crate::plans::CreateFileFormatPlan; use crate::plans::CreateIndexPlan; use crate::plans::CreateNetworkPolicyPlan; +use crate::plans::CreatePasswordPolicyPlan; use crate::plans::CreateRolePlan; use crate::plans::CreateShareEndpointPlan; use crate::plans::CreateSharePlan; @@ -61,6 +63,7 @@ use crate::plans::DeletePlan; use crate::plans::DescConnectionPlan; use crate::plans::DescDatamaskPolicyPlan; use crate::plans::DescNetworkPolicyPlan; +use crate::plans::DescPasswordPolicyPlan; use crate::plans::DescSharePlan; use crate::plans::DescribeTablePlan; use crate::plans::DescribeTaskPlan; @@ -71,6 +74,7 @@ use crate::plans::DropDatamaskPolicyPlan; use crate::plans::DropFileFormatPlan; use crate::plans::DropIndexPlan; use crate::plans::DropNetworkPolicyPlan; +use crate::plans::DropPasswordPolicyPlan; use crate::plans::DropRolePlan; use crate::plans::DropShareEndpointPlan; use crate::plans::DropSharePlan; @@ -301,6 +305,12 @@ pub enum Plan { DescNetworkPolicy(Box<DescNetworkPolicyPlan>), ShowNetworkPolicies(Box<ShowNetworkPoliciesPlan>), + // Password policy + CreatePasswordPolicy(Box<CreatePasswordPolicyPlan>), + AlterPasswordPolicy(Box<AlterPasswordPolicyPlan>), + DropPasswordPolicy(Box<DropPasswordPolicyPlan>), + DescPasswordPolicy(Box<DescPasswordPolicyPlan>), + // Task CreateTask(Box<CreateTaskPlan>), AlterTask(Box<AlterTaskPlan>), @@ -337,6 +347,7 @@ pub enum RewriteKind { DescribeStage, ListStage, ShowRoles, + ShowPasswordPolicies, Call, } @@ -407,11 +418,9 @@ impl Plan { Plan::CreateDatamaskPolicy(plan) => plan.schema(), Plan::DropDatamaskPolicy(plan) => plan.schema(), Plan::DescDatamaskPolicy(plan) => plan.schema(), - Plan::CreateNetworkPolicy(plan) => plan.schema(), - Plan::AlterNetworkPolicy(plan) => plan.schema(), - Plan::DropNetworkPolicy(plan) => plan.schema(), Plan::DescNetworkPolicy(plan) => plan.schema(), Plan::ShowNetworkPolicies(plan) => plan.schema(), + Plan::DescPasswordPolicy(plan) => plan.schema(), Plan::CopyIntoTable(plan) => plan.schema(), Plan::MergeInto(plan) => plan.schema(), Plan::CreateTask(plan) => plan.schema(), @@ -455,6 +464,7 @@ impl Plan { | Plan::DescDatamaskPolicy(_) | Plan::DescNetworkPolicy(_) | Plan::ShowNetworkPolicies(_) + | Plan::DescPasswordPolicy(_) | Plan::CopyIntoTable(_) | Plan::ShowTasks(_) | Plan::DescribeTask(_) diff --git a/src/query/storages/system/src/lib.rs b/src/query/storages/system/src/lib.rs index 977884955a9b1..f7edbdbbdf07e 100644 --- a/src/query/storages/system/src/lib.rs +++ b/src/query/storages/system/src/lib.rs @@ -40,6 +40,7 @@ mod malloc_stats_table; mod malloc_stats_totals_table; mod metrics_table; mod one_table; +mod password_policies_table; mod processes_table; mod processor_profile_table; mod query_cache_table; @@ -87,6 +88,7 @@ pub use malloc_stats_table::MallocStatsTable; pub use malloc_stats_totals_table::MallocStatsTotalsTable; pub use metrics_table::MetricsTable; pub use one_table::OneTable; +pub use password_policies_table::PasswordPoliciesTable; pub use processes_table::ProcessesTable; pub use processor_profile_table::ProcessorProfileTable; pub use query_cache_table::QueryCacheTable; diff --git a/src/query/storages/system/src/password_policies_table.rs b/src/query/storages/system/src/password_policies_table.rs new file mode 100644 index 0000000000000..dc83061ccc7c6 --- /dev/null +++ b/src/query/storages/system/src/password_policies_table.rs @@ -0,0 +1,130 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; + +use databend_common_catalog::plan::PushDownInfo; +use databend_common_catalog::table::Table; +use databend_common_catalog::table_context::TableContext; +use databend_common_exception::Result; +use databend_common_expression::types::StringType; +use databend_common_expression::types::TimestampType; +use databend_common_expression::utils::FromData; +use databend_common_expression::DataBlock; +use databend_common_expression::TableDataType; +use databend_common_expression::TableField; +use databend_common_expression::TableSchemaRefExt; +use databend_common_meta_app::schema::TableIdent; +use databend_common_meta_app::schema::TableInfo; +use databend_common_meta_app::schema::TableMeta; +use databend_common_users::UserApiProvider; + +use crate::table::AsyncOneBlockSystemTable; +use crate::table::AsyncSystemTable; + +pub struct PasswordPoliciesTable { + table_info: TableInfo, +} + +#[async_trait::async_trait] +impl AsyncSystemTable for PasswordPoliciesTable { + const NAME: &'static str = "system.password_policies"; + + fn get_table_info(&self) -> &TableInfo { + &self.table_info + } + + #[async_backtrace::framed] + async fn get_full_data( + &self, + ctx: Arc<dyn TableContext>, + _push_downs: Option<PushDownInfo>, + ) -> Result<DataBlock> { + let tenant = ctx.get_tenant(); + let password_policies = UserApiProvider::instance() + .get_password_policies(&tenant) + .await?; + + let mut names = Vec::with_capacity(password_policies.len()); + let mut comments = Vec::with_capacity(password_policies.len()); + let mut options = Vec::with_capacity(password_policies.len()); + let mut created_ons = Vec::with_capacity(password_policies.len()); + let mut updated_ons = Vec::with_capacity(password_policies.len()); + for password_policy in password_policies { + names.push(password_policy.name.as_bytes().to_vec()); + comments.push(password_policy.comment.as_bytes().to_vec()); + + let values = vec![ + format!("MIN_LENGTH={}", password_policy.min_length), + format!("MAX_LENGTH={}", password_policy.max_length), + format!( + "MIN_UPPER_CASE_CHARS={}", + password_policy.min_upper_case_chars + ), + format!( + "MIN_LOWER_CASE_CHARS={}", + password_policy.min_lower_case_chars + ), + format!("MIN_NUMERIC_CHARS={}", password_policy.min_numeric_chars), + format!("MIN_SPECIAL_CHARS={}", password_policy.min_special_chars), + format!("MIN_AGE_DAYS={}", password_policy.min_age_days), + format!("MAX_AGE_DAYS={}", password_policy.max_age_days), + format!("MAX_RETRIES={}", password_policy.max_retries), + format!("LOCKOUT_TIME_MINS={}", password_policy.lockout_time_mins), + format!("HISTORY={}", password_policy.history), + ]; + let option = values.join(", "); + options.push(option.as_bytes().to_vec()); + + created_ons.push(password_policy.create_on.timestamp_micros()); + updated_ons.push(password_policy.update_on.map(|u| u.timestamp_micros())); + } + + Ok(DataBlock::new_from_columns(vec![ + StringType::from_data(names), + StringType::from_data(comments), + StringType::from_data(options), + TimestampType::from_data(created_ons), + TimestampType::from_opt_data(updated_ons), + ])) + } +} + +impl PasswordPoliciesTable { + pub fn create(table_id: u64) -> Arc<dyn Table> { + let schema = TableSchemaRefExt::create(vec![ + TableField::new("name", TableDataType::String), + TableField::new("comment", TableDataType::String), + TableField::new("options", TableDataType::String), + TableField::new("created_on", TableDataType::Timestamp), + TableField::new( + "updated_on", + TableDataType::Nullable(Box::new(TableDataType::Timestamp)), + ), + ]); + + let table_info = TableInfo { + desc: "'system'.'password_policies'".to_string(), + name: "password_policies".to_string(), + ident: TableIdent::new(table_id, 0), + meta: TableMeta { + schema, + engine: "SystemPasswordPolicies".to_string(), + ..Default::default() + }, + ..Default::default() + }; + AsyncOneBlockSystemTable::create(PasswordPoliciesTable { table_info }) + } +} diff --git a/src/query/users/src/lib.rs b/src/query/users/src/lib.rs index f3f605d43f031..effe3ac97b395 100644 --- a/src/query/users/src/lib.rs +++ b/src/query/users/src/lib.rs @@ -20,6 +20,7 @@ extern crate core; mod jwt; mod network_policy; +mod password_policy; mod role_mgr; mod user; mod user_api; @@ -36,6 +37,7 @@ pub mod role_cache_mgr; pub mod role_util; pub use jwt::*; +pub use password_policy::*; pub use role_cache_mgr::RoleCacheManager; pub use role_mgr::BUILTIN_ROLE_ACCOUNT_ADMIN; pub use role_mgr::BUILTIN_ROLE_PUBLIC; diff --git a/src/query/users/src/password_policy.rs b/src/query/users/src/password_policy.rs new file mode 100644 index 0000000000000..2fc98cdff50e1 --- /dev/null +++ b/src/query/users/src/password_policy.rs @@ -0,0 +1,354 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use chrono::Utc; +use databend_common_exception::ErrorCode; +use databend_common_exception::Result; +use databend_common_management::PasswordPolicyApi; +use databend_common_meta_app::principal::PasswordPolicy; +use databend_common_meta_types::MatchSeq; + +use crate::UserApiProvider; + +// default value of password policy options +pub const DEFAULT_PASSWORD_MIN_LENGTH: u64 = 8; +pub const DEFAULT_PASSWORD_MAX_LENGTH: u64 = 256; +pub const DEFAULT_PASSWORD_MIN_CHARS: u64 = 1; +pub const DEFAULT_PASSWORD_MIN_SPECIAL_CHARS: u64 = 0; +pub const DEFAULT_PASSWORD_MIN_AGE_DAYS: u64 = 0; +pub const DEFAULT_PASSWORD_MAX_AGE_DAYS: u64 = 90; +pub const DEFAULT_PASSWORD_MAX_RETRIES: u64 = 5; +pub const DEFAULT_PASSWORD_LOCKOUT_TIME_MINS: u64 = 15; +pub const DEFAULT_PASSWORD_HISTORY: u64 = 0; + +// minimal value of password policy options +pub const MIN_PASSWORD_LENGTH: u64 = 8; +pub const MIN_PASSWORD_CHARS: u64 = 0; +pub const MIN_PASSWORD_AGE_DAYS: u64 = 0; +pub const MIN_PASSWORD_MAX_RETRIES: u64 = 1; +pub const MIN_PASSWORD_LOCKOUT_TIME_MINS: u64 = 1; +pub const MIN_PASSWORD_HISTORY: u64 = 0; + +// maximum value of password policy options +pub const MAX_PASSWORD_LENGTH: u64 = 256; +pub const MAX_PASSWORD_CHARS: u64 = 256; +pub const MAX_PASSWORD_AGE_DAYS: u64 = 999; +pub const MAX_PASSWORD_MAX_RETRIES: u64 = 10; +pub const MAX_PASSWORD_LOCKOUT_TIME_MINS: u64 = 999; +pub const MAX_PASSWORD_HISTORY: u64 = 24; + +impl UserApiProvider { + // Add a new password policy. + #[async_backtrace::framed] + pub async fn add_password_policy( + &self, + tenant: &str, + password_policy: PasswordPolicy, + if_not_exists: bool, + ) -> Result<u64> { + check_password_policy(&password_policy)?; + + if if_not_exists + && self + .exists_password_policy(tenant, password_policy.name.as_str()) + .await? + { + return Ok(0); + } + + let client = self.get_password_policy_api_client(tenant)?; + let add_password_policy = client.add_password_policy(password_policy); + match add_password_policy.await { + Ok(res) => Ok(res), + Err(e) => { + if if_not_exists && e.code() == ErrorCode::PASSWORD_POLICY_ALREADY_EXISTS { + Ok(0) + } else { + Err(e.add_message_back("(while add password policy)")) + } + } + } + } + + // Update password policy. + #[async_backtrace::framed] + #[allow(clippy::too_many_arguments)] + pub async fn update_password_policy( + &self, + tenant: &str, + name: &str, + min_length: Option<u64>, + max_length: Option<u64>, + min_upper_case_chars: Option<u64>, + min_lower_case_chars: Option<u64>, + min_numeric_chars: Option<u64>, + min_special_chars: Option<u64>, + min_age_days: Option<u64>, + max_age_days: Option<u64>, + max_retries: Option<u64>, + lockout_time_mins: Option<u64>, + history: Option<u64>, + comment: Option<String>, + if_exists: bool, + ) -> Result<Option<u64>> { + let client = self.get_password_policy_api_client(tenant)?; + let seq_password_policy = match client.get_password_policy(name, MatchSeq::GE(0)).await { + Ok(seq_password_policy) => seq_password_policy, + Err(e) => { + if if_exists && e.code() == ErrorCode::UNKNOWN_PASSWORD_POLICY { + return Ok(None); + } else { + return Err(e.add_message_back(" (while alter password policy)")); + } + } + }; + + let seq = seq_password_policy.seq; + let mut password_policy = seq_password_policy.data; + if let Some(min_length) = min_length { + password_policy.min_length = min_length; + } + if let Some(max_length) = max_length { + password_policy.max_length = max_length; + } + if let Some(min_upper_case_chars) = min_upper_case_chars { + password_policy.min_upper_case_chars = min_upper_case_chars; + } + if let Some(min_lower_case_chars) = min_lower_case_chars { + password_policy.min_lower_case_chars = min_lower_case_chars; + } + if let Some(min_numeric_chars) = min_numeric_chars { + password_policy.min_numeric_chars = min_numeric_chars; + } + if let Some(min_special_chars) = min_special_chars { + password_policy.min_special_chars = min_special_chars; + } + if let Some(min_age_days) = min_age_days { + password_policy.min_age_days = min_age_days; + } + if let Some(max_age_days) = max_age_days { + password_policy.max_age_days = max_age_days; + } + if let Some(max_retries) = max_retries { + password_policy.max_retries = max_retries; + } + if let Some(lockout_time_mins) = lockout_time_mins { + password_policy.lockout_time_mins = lockout_time_mins; + } + if let Some(history) = history { + password_policy.history = history; + } + if let Some(comment) = comment { + password_policy.comment = comment; + } + check_password_policy(&password_policy)?; + + password_policy.update_on = Some(Utc::now()); + + match client + .update_password_policy(password_policy, MatchSeq::Exact(seq)) + .await + { + Ok(res) => Ok(Some(res)), + Err(e) => Err(e.add_message_back(" (while alter password policy).")), + } + } + + // Drop a password policy by name. + #[async_backtrace::framed] + pub async fn drop_password_policy( + &self, + tenant: &str, + name: &str, + if_exists: bool, + ) -> Result<()> { + let user_infos = self.get_users(tenant).await?; + for user_info in user_infos { + if let Some(network_policy) = user_info.option.password_policy() { + if network_policy == name { + return Err(ErrorCode::PasswordPolicyIsUsedByUser(format!( + "password policy `{}` is used by user", + name, + ))); + } + } + } + + let client = self.get_password_policy_api_client(tenant)?; + match client.drop_password_policy(name, MatchSeq::GE(1)).await { + Ok(res) => Ok(res), + Err(e) => { + if if_exists && e.code() == ErrorCode::UNKNOWN_PASSWORD_POLICY { + Ok(()) + } else { + Err(e.add_message_back(" (while drop password policy)")) + } + } + } + } + + // Check whether a password policy is exist. + #[async_backtrace::framed] + pub async fn exists_password_policy(&self, tenant: &str, name: &str) -> Result<bool> { + match self.get_password_policy(tenant, name).await { + Ok(_) => Ok(true), + Err(e) => { + if e.code() == ErrorCode::UNKNOWN_PASSWORD_POLICY { + Ok(false) + } else { + Err(e) + } + } + } + } + + // Get a password_policy by tenant. + #[async_backtrace::framed] + pub async fn get_password_policy(&self, tenant: &str, name: &str) -> Result<PasswordPolicy> { + let client = self.get_password_policy_api_client(tenant)?; + let password_policy = client + .get_password_policy(name, MatchSeq::GE(0)) + .await? + .data; + Ok(password_policy) + } + + // Get all password policies by tenant. + #[async_backtrace::framed] + pub async fn get_password_policies(&self, tenant: &str) -> Result<Vec<PasswordPolicy>> { + let client = self.get_password_policy_api_client(tenant)?; + let password_policies = client + .get_password_policies() + .await + .map_err(|e| e.add_message_back(" (while get password policies)."))?; + Ok(password_policies) + } +} + +// Check whether the values of options in the password policy are valid +fn check_password_policy(password_policy: &PasswordPolicy) -> Result<()> { + if !(MIN_PASSWORD_LENGTH..=MAX_PASSWORD_LENGTH).contains(&password_policy.min_length) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min length, supported range: {} to {}, but got {}", + MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, password_policy.min_length + ))); + } + + if !(MIN_PASSWORD_LENGTH..=MAX_PASSWORD_LENGTH).contains(&password_policy.max_length) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password max length, supported range: {} to {}, but got {}", + MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH, password_policy.max_length + ))); + } + + // min length can't greater than max length + if password_policy.min_length > password_policy.max_length { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password length, min length must be less than max length, but got {} and {}", + password_policy.min_length, password_policy.max_length + ))); + } + + if !(MIN_PASSWORD_CHARS..=MAX_PASSWORD_CHARS).contains(&password_policy.min_upper_case_chars) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min upper case chars, supported range: {} to {}, but got {}", + MIN_PASSWORD_CHARS, MAX_PASSWORD_CHARS, password_policy.min_upper_case_chars + ))); + } + + if !(MIN_PASSWORD_CHARS..=MAX_PASSWORD_CHARS).contains(&password_policy.min_lower_case_chars) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min lower case chars, supported range: {} to {}, but got {}", + MIN_PASSWORD_CHARS, MAX_PASSWORD_CHARS, password_policy.min_lower_case_chars + ))); + } + + if !(MIN_PASSWORD_CHARS..=MAX_PASSWORD_CHARS).contains(&password_policy.min_numeric_chars) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min numeric chars, supported range: {} to {}, but got {}", + MIN_PASSWORD_CHARS, MAX_PASSWORD_CHARS, password_policy.min_numeric_chars + ))); + } + + if !(MIN_PASSWORD_CHARS..=MAX_PASSWORD_CHARS).contains(&password_policy.min_special_chars) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min special chars, supported range: {} to {}, but got {}", + MIN_PASSWORD_CHARS, MAX_PASSWORD_CHARS, password_policy.min_special_chars + ))); + } + + // sum min length of chars can't greater than max length + let char_length = password_policy.min_upper_case_chars + + password_policy.min_lower_case_chars + + password_policy.min_numeric_chars + + password_policy.min_special_chars; + if char_length > password_policy.max_length { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password length, sum of min chars length must be less than max length, but got + min upper case chars {}, min lower case chars {}, min numeric chars {}, min special chars {} + and max length {}", + password_policy.min_upper_case_chars, password_policy.min_lower_case_chars, + password_policy.min_numeric_chars, password_policy.min_special_chars, password_policy.max_length + ))); + } + + if !(MIN_PASSWORD_AGE_DAYS..=MAX_PASSWORD_AGE_DAYS).contains(&password_policy.min_age_days) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password min age days, supported range: {} to {}, but got {}", + MIN_PASSWORD_AGE_DAYS, MAX_PASSWORD_AGE_DAYS, password_policy.min_age_days + ))); + } + + if !(MIN_PASSWORD_AGE_DAYS..=MAX_PASSWORD_AGE_DAYS).contains(&password_policy.max_age_days) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password max age days, supported range: {} to {}, but got {}", + MIN_PASSWORD_AGE_DAYS, MAX_PASSWORD_AGE_DAYS, password_policy.max_age_days + ))); + } + + if password_policy.min_age_days > password_policy.max_age_days { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password age days, min age days must be less than max age days, but got {} and {}", + password_policy.min_age_days, password_policy.max_age_days + ))); + } + + if !(MIN_PASSWORD_MAX_RETRIES..=MAX_PASSWORD_MAX_RETRIES).contains(&password_policy.max_retries) + { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password max retries, supported range: {} to {}, but got {}", + MIN_PASSWORD_MAX_RETRIES, MAX_PASSWORD_MAX_RETRIES, password_policy.max_retries + ))); + } + + if !(MIN_PASSWORD_LOCKOUT_TIME_MINS..=MAX_PASSWORD_LOCKOUT_TIME_MINS) + .contains(&password_policy.lockout_time_mins) + { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password lockout time mins, supported range: {} to {}, but got {}", + MIN_PASSWORD_LOCKOUT_TIME_MINS, + MAX_PASSWORD_LOCKOUT_TIME_MINS, + password_policy.lockout_time_mins + ))); + } + + if !(MIN_PASSWORD_HISTORY..=MAX_PASSWORD_HISTORY).contains(&password_policy.history) { + return Err(ErrorCode::InvalidArgument(format!( + "invalid password history, supported range: {} to {}, but got {}", + MIN_PASSWORD_HISTORY, MAX_PASSWORD_HISTORY, password_policy.history + ))); + } + + Ok(()) +} diff --git a/src/query/users/src/user_api.rs b/src/query/users/src/user_api.rs index 9e73a401b3acf..96a71dcf3b487 100644 --- a/src/query/users/src/user_api.rs +++ b/src/query/users/src/user_api.rs @@ -24,6 +24,8 @@ use databend_common_management::FileFormatApi; use databend_common_management::FileFormatMgr; use databend_common_management::NetworkPolicyApi; use databend_common_management::NetworkPolicyMgr; +use databend_common_management::PasswordPolicyApi; +use databend_common_management::PasswordPolicyMgr; use databend_common_management::QuotaApi; use databend_common_management::QuotaMgr; use databend_common_management::RoleApi; @@ -139,6 +141,16 @@ impl UserApiProvider { )?)) } + pub fn get_password_policy_api_client( + &self, + tenant: &str, + ) -> Result<Arc<impl PasswordPolicyApi>> { + Ok(Arc::new(PasswordPolicyMgr::create( + self.client.clone(), + tenant, + )?)) + } + pub fn get_meta_store_client(&self) -> Arc<MetaStore> { Arc::new(self.meta.clone()) } diff --git a/src/query/users/src/user_mgr.rs b/src/query/users/src/user_mgr.rs index 77eb4ab6118a5..92ced22a34f88 100644 --- a/src/query/users/src/user_mgr.rs +++ b/src/query/users/src/user_mgr.rs @@ -144,6 +144,14 @@ impl UserApiProvider { ))); } } + if let Some(name) = user_info.option.password_policy() { + if self.get_password_policy(tenant, name).await.is_err() { + return Err(ErrorCode::UnknownPasswordPolicy(format!( + "password policy `{}` is not exist", + name + ))); + } + } if self.get_configured_user(&user_info.name).is_some() { return Err(ErrorCode::UserAlreadyExists(format!( "Same name with configured user `{}`", @@ -295,6 +303,14 @@ impl UserApiProvider { ))); } } + if let Some(name) = user_option.password_policy() { + if self.get_password_policy(tenant, name).await.is_err() { + return Err(ErrorCode::UnknownPasswordPolicy(format!( + "password policy `{}` is not exist", + name + ))); + } + } } if self.get_configured_user(&user.username).is_some() { return Err(ErrorCode::UserAlreadyExists(format!( diff --git a/src/query/users/tests/it/mod.rs b/src/query/users/tests/it/mod.rs index 3050ad50e21f4..70307c856b255 100644 --- a/src/query/users/tests/it/mod.rs +++ b/src/query/users/tests/it/mod.rs @@ -14,6 +14,7 @@ mod jwt; mod network_policy; +mod password_policy; mod role_cache_mgr; mod role_mgr; mod user_mgr; diff --git a/src/query/users/tests/it/password_policy.rs b/src/query/users/tests/it/password_policy.rs new file mode 100644 index 0000000000000..db5672104f9c2 --- /dev/null +++ b/src/query/users/tests/it/password_policy.rs @@ -0,0 +1,162 @@ +// Copyright 2021 Datafuse Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use databend_common_base::base::tokio; +use databend_common_exception::Result; +use databend_common_grpc::RpcClientConf; +use databend_common_meta_app::principal::AuthInfo; +use databend_common_meta_app::principal::PasswordPolicy; +use databend_common_meta_app::principal::UserIdentity; +use databend_common_meta_app::principal::UserInfo; +use databend_common_users::UserApiProvider; +use pretty_assertions::assert_eq; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_password_policy() -> Result<()> { + let conf = RpcClientConf::default(); + let user_mgr = UserApiProvider::try_create_simple(conf).await?; + + let tenant = "test"; + let username = "test-user1"; + let hostname = "%"; + let pwd = "test-pwd"; + + let policy_name = "test_policy".to_string(); + + // add password policy + let password_policy = PasswordPolicy { + name: policy_name.clone(), + min_length: 12, + max_length: 24, + min_upper_case_chars: 2, + min_lower_case_chars: 2, + min_numeric_chars: 2, + min_special_chars: 2, + min_age_days: 1, + max_age_days: 30, + max_retries: 3, + lockout_time_mins: 30, + history: 5, + comment: "".to_string(), + create_on: Utc.with_ymd_and_hms(2023, 12, 19, 12, 0, 0).unwrap(), + update_on: None, + }; + + let res = user_mgr.add_password_policy(tenant, password_policy.clone(), false).await; + assert!(res.ok()); + + // invalid min length + let mut invalid_password_policy1 = password_policy.clone(); + invalid_password_policy1.min_length = 0; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy1, false).await; + assert!(res.is_err()); + + // invalid max length + let mut invalid_password_policy2 = password_policy.clone(); + invalid_password_policy2.max_length = 260; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy2, false).await; + assert!(res.is_err()); + + // invalid min length greater than max length + let mut invalid_password_policy3 = password_policy.clone(); + invalid_password_policy3.min_length = 30; + invalid_password_policy3.max_length = 20; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy2, false).await; + assert!(res.is_err()); + + // invalid min chars + let mut invalid_password_policy4 = password_policy.clone(); + invalid_password_policy4.min_upper_case_chars = 270; + invalid_password_policy4.min_lower_case_chars = 271; + invalid_password_policy4.min_numeric_chars = 272; + invalid_password_policy4.min_special_chars = 273; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy4, false).await; + assert!(res.is_err()); + + // invalid sum of upper chars, lower chars, numeric chars and special chars greater than max length + let mut invalid_password_policy5 = password_policy.clone(); + invalid_password_policy5.max_length = 30; + invalid_password_policy5.min_upper_case_chars = 10; + invalid_password_policy5.min_lower_case_chars = 11; + invalid_password_policy5.min_numeric_chars = 12; + invalid_password_policy5.min_special_chars = 13; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy5, false).await; + assert!(res.is_err()); + + // invalid min age days greater than max age days + let mut invalid_password_policy6 = password_policy.clone(); + invalid_password_policy6.min_age_days = 20; + invalid_password_policy6.max_age_days = 10; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy6, false).await; + assert!(res.is_err()); + + // invalid max retries + let mut invalid_password_policy7 = password_policy.clone(); + invalid_password_policy7.max_retries = 20; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy7, false).await; + assert!(res.is_err()); + + // invalid lockout time mins + let mut invalid_password_policy8 = password_policy.clone(); + invalid_password_policy8.lockout_time_mins = 2000; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy8, false).await; + assert!(res.is_err()); + + // invalid history + let mut invalid_password_policy9 = password_policy.clone(); + invalid_password_policy9.history = 50; + let res = user_mgr.add_password_policy(tenant, invalid_password_policy9, false).await; + assert!(res.is_err()); + + // update password policy + let res = user_mgr.update_password_policy(tenant, &policy_name, + Some(10), + Some(20), + Some(3), + Some(3), + Some(3), + Some(3), + Some(2), + Some(50), + Some(10), + Some(20), + Some(10), + None, + false).await; + assert!(res.ok()); + + // add user + let auth_info = AuthInfo::Password { + hash_value: Vec::from(pwd), + hash_method: PasswordHashMethod::Sha256, + }; + + let mut user_info = UserInfo::new(username, hostname, auth_info.clone()); + let mut option = UserOption::empty(); + option = option + .with_password_policy(Some(policy_name.clone())); + user_info.update_auth_option(None, Some(option)) + user_mgr.add_user(tenant, user_info, false).await?; + + // drop password policy + let res = user_mgr.drop_password_policy(tenant, policy_name.as_ref(), false).await; + assert!(res.is_err()); + + user_mgr.drop_user(tenant, user.clone(), false).await?; + + let res = user_mgr.drop_password_policy(tenant, policy_name.as_ref(), false).await; + assert!(res.is_ok()); + + Ok(()) +} diff --git a/tests/sqllogictests/suites/base/05_ddl/05_0034_ddl_password_policy.test b/tests/sqllogictests/suites/base/05_ddl/05_0034_ddl_password_policy.test new file mode 100644 index 0000000000000..dd9a1eb407e7e --- /dev/null +++ b/tests/sqllogictests/suites/base/05_ddl/05_0034_ddl_password_policy.test @@ -0,0 +1,134 @@ +statement ok +DROP PASSWORD POLICY IF EXISTS test_policy + +statement ok +DROP PASSWORD POLICY IF EXISTS default_policy + +statement error 2211 +DROP PASSWORD POLICY test_policy + +statement ok +CREATE PASSWORD POLICY test_policy + PASSWORD_MIN_LENGTH = 12 + PASSWORD_MAX_LENGTH = 24 + PASSWORD_MIN_UPPER_CASE_CHARS = 2 + PASSWORD_MIN_LOWER_CASE_CHARS = 2 + PASSWORD_MIN_NUMERIC_CHARS = 2 + PASSWORD_MIN_SPECIAL_CHARS = 2 + PASSWORD_MIN_AGE_DAYS = 1 + PASSWORD_MAX_AGE_DAYS = 30 + PASSWORD_MAX_RETRIES = 3 + PASSWORD_LOCKOUT_TIME_MINS = 30 + PASSWORD_HISTORY = 5 + COMMENT = 'test comment' + +query TTTT +DESC PASSWORD POLICY test_policy +---- +NAME test_policy NULL Name of password policy. +COMMENT test comment NULL Comment of password policy. +PASSWORD_MIN_LENGTH 12 8 Minimum length of new password. +PASSWORD_MAX_LENGTH 24 256 Maximum length of new password. +PASSWORD_MIN_UPPER_CASE_CHARS 2 1 Minimum number of uppercase characters in new password. +PASSWORD_MIN_LOWER_CASE_CHARS 2 1 Minimum number of lowercase characters in new password. +PASSWORD_MIN_NUMERIC_CHARS 2 1 Minimum number of numeric characters in new password. +PASSWORD_MIN_SPECIAL_CHARS 2 0 Minimum number of special characters in new password. +PASSWORD_MIN_AGE_DAYS 1 0 Period after a password is changed during which a password cannot be changed again, in days. +PASSWORD_MAX_AGE_DAYS 30 90 Period after which password must be changed, in days. +PASSWORD_MAX_RETRIES 3 5 Number of attempts users have to enter the correct password before their account is locked. +PASSWORD_LOCKOUT_TIME_MINS 30 15 Period of time for which users will be locked after entering their password incorrectly many times (specified by MAX_RETRIES), in minutes. +PASSWORD_HISTORY 5 0 Number of most recent passwords that may not be repeated by the user. + +statement error 2004 +CREATE PASSWORD POLICY default_policy PASSWORD_MIN_LENGTH = 1 + +statement error 2004 +CREATE PASSWORD POLICY default_policy PASSWORD_MAX_LENGTH = 1000 + +statement error 2004 +CREATE PASSWORD POLICY default_policy + PASSWORD_MIN_LENGTH = 12 + PASSWORD_MAX_LENGTH = 24 + PASSWORD_MIN_UPPER_CASE_CHARS = 10 + PASSWORD_MIN_LOWER_CASE_CHARS = 10 + PASSWORD_MIN_NUMERIC_CHARS = 10 + PASSWORD_MIN_SPECIAL_CHARS = 10 + +statement ok +CREATE PASSWORD POLICY default_policy COMMENT = 'default values' + +query TTT +SHOW PASSWORD POLICIES +---- +default_policy default values MIN_LENGTH=8, MAX_LENGTH=256, MIN_UPPER_CASE_CHARS=1, MIN_LOWER_CASE_CHARS=1, MIN_NUMERIC_CHARS=1, MIN_SPECIAL_CHARS=0, MIN_AGE_DAYS=0, MAX_AGE_DAYS=90, MAX_RETRIES=5, LOCKOUT_TIME_MINS=15, HISTORY=0 +test_policy test comment MIN_LENGTH=12, MAX_LENGTH=24, MIN_UPPER_CASE_CHARS=2, MIN_LOWER_CASE_CHARS=2, MIN_NUMERIC_CHARS=2, MIN_SPECIAL_CHARS=2, MIN_AGE_DAYS=1, MAX_AGE_DAYS=30, MAX_RETRIES=3, LOCKOUT_TIME_MINS=30, HISTORY=5 + +statement ok +ALTER PASSWORD POLICY default_policy SET + PASSWORD_MIN_LENGTH = 10 + PASSWORD_MAX_LENGTH = 25 + PASSWORD_HISTORY = 8 + +statement ok +ALTER PASSWORD POLICY test_policy UNSET + PASSWORD_MIN_LENGTH + PASSWORD_MAX_LENGTH + PASSWORD_LOCKOUT_TIME_MINS + +query TTT +SHOW PASSWORD POLICIES +---- +default_policy default values MIN_LENGTH=10, MAX_LENGTH=25, MIN_UPPER_CASE_CHARS=1, MIN_LOWER_CASE_CHARS=1, MIN_NUMERIC_CHARS=1, MIN_SPECIAL_CHARS=0, MIN_AGE_DAYS=0, MAX_AGE_DAYS=90, MAX_RETRIES=5, LOCKOUT_TIME_MINS=15, HISTORY=8 +test_policy test comment MIN_LENGTH=8, MAX_LENGTH=256, MIN_UPPER_CASE_CHARS=2, MIN_LOWER_CASE_CHARS=2, MIN_NUMERIC_CHARS=2, MIN_SPECIAL_CHARS=2, MIN_AGE_DAYS=1, MAX_AGE_DAYS=30, MAX_RETRIES=3, LOCKOUT_TIME_MINS=15, HISTORY=5 + +query TTT +SHOW PASSWORD POLICIES like 'test%' +---- +test_policy test comment MIN_LENGTH=8, MAX_LENGTH=256, MIN_UPPER_CASE_CHARS=2, MIN_LOWER_CASE_CHARS=2, MIN_NUMERIC_CHARS=2, MIN_SPECIAL_CHARS=2, MIN_AGE_DAYS=1, MAX_AGE_DAYS=30, MAX_RETRIES=3, LOCKOUT_TIME_MINS=15, HISTORY=5 + +query TTT +SHOW PASSWORD POLICIES like 'default%' +---- +default_policy default values MIN_LENGTH=10, MAX_LENGTH=25, MIN_UPPER_CASE_CHARS=1, MIN_LOWER_CASE_CHARS=1, MIN_NUMERIC_CHARS=1, MIN_SPECIAL_CHARS=0, MIN_AGE_DAYS=0, MAX_AGE_DAYS=90, MAX_RETRIES=5, LOCKOUT_TIME_MINS=15, HISTORY=8 + +statement ok +DROP USER IF EXISTS user1 + +statement error 2211 +CREATE USER user1 IDENTIFIED BY '123456' WITH SET PASSWORD POLICY='test_policy2' + +statement error 2215 +CREATE USER user1 IDENTIFIED BY '123456' WITH SET PASSWORD POLICY='default_policy' + +statement error 2215 +CREATE USER user1 IDENTIFIED BY '123456abc' WITH SET PASSWORD POLICY='default_policy' + +statement ok +CREATE USER user1 IDENTIFIED BY '123456abcD' WITH SET PASSWORD POLICY='default_policy' + +statement error 2215 +ALTER USER user1 IDENTIFIED BY '123456abcd' + +statement error 2215 +ALTER USER user1 IDENTIFIED BY '123456abcDE' WITH SET PASSWORD POLICY='test_policy' + +statement ok +ALTER USER user1 IDENTIFIED BY '123456abcDE@!' WITH SET PASSWORD POLICY='test_policy' + +statement error 2214 +DROP PASSWORD POLICY test_policy + +statement ok +ALTER USER user1 IDENTIFIED BY '123456' WITH UNSET PASSWORD POLICY + +statement ok +DROP PASSWORD POLICY test_policy + +statement error 2211 +DROP PASSWORD POLICY test_policy + +statement error 2211 +DESC PASSWORD POLICY test_policy + +statement ok +DROP PASSWORD POLICY default_policy