mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Finalise email changes for oidc (#629)
This commit is contained in:
parent
dc1dd11333
commit
c6c564cebb
|
@ -125,12 +125,16 @@ You can create a scope map with:
|
|||
> scope map OR implicit scope named 'openid'. Without this, openid clients *WILL NOT WORK*
|
||||
|
||||
> **HINT**
|
||||
> openid connect provides a number of scopes that affect the content of the resulting
|
||||
> authorisation token. Supported scopes and their associated claims are:
|
||||
> openid connect allows a number of scopes that affect the content of the resulting
|
||||
> authorisation token. If one of the following scopes are requested by the openid client,
|
||||
> then the associated claims may be added to the authorisation token. It is not guaranteed
|
||||
> that all of the associated claims will be added.
|
||||
>
|
||||
> * profile - (name, family\_name, given\_name, middle\_name, nickname, preferred\_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated\_at)
|
||||
> * email - (email, email\_verified)
|
||||
> * address - (address)
|
||||
> * phone - (phone\_number, phone\_number\_verified)
|
||||
>
|
||||
|
||||
Once created you can view the details of the resource server.
|
||||
|
||||
|
|
|
@ -1278,6 +1278,20 @@ impl KanidmAsyncClient {
|
|||
.await
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn idm_account_orgperson_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
let x = AccountOrgPersonExtend {
|
||||
mail: mail.to_string(),
|
||||
};
|
||||
self.perform_post_request(format!("/v1/account/{}/_orgperson", id).as_str(), x)
|
||||
.await
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str())
|
||||
.await
|
||||
|
@ -1294,8 +1308,17 @@ impl KanidmAsyncClient {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_person_extend(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), ())
|
||||
pub async fn idm_account_person_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: Option<&[String]>,
|
||||
legalname: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
let px = AccountPersonExtend {
|
||||
mail: mail.map(|s| s.to_vec()),
|
||||
legalname: legalname.map(str::to_string),
|
||||
};
|
||||
self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), px)
|
||||
.await
|
||||
}
|
||||
|
||||
|
|
|
@ -810,6 +810,16 @@ impl KanidmClient {
|
|||
tokio_block_on(self.asclient.idm_account_unix_cred_verify(id, cred))
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn idm_account_orgperson_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: &str,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_orgperson_extend(id, mail))
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_get_ssh_pubkeys(id))
|
||||
}
|
||||
|
@ -823,8 +833,13 @@ impl KanidmClient {
|
|||
tokio_block_on(self.asclient.idm_account_post_ssh_pubkey(id, tag, pubkey))
|
||||
}
|
||||
|
||||
pub fn idm_account_person_extend(&self, id: &str) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_person_extend(id))
|
||||
pub fn idm_account_person_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
mail: Option<&[String]>,
|
||||
legalname: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
tokio_block_on(self.asclient.idm_account_person_extend(id, mail, legalname))
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -123,7 +123,7 @@ fn add_all_attrs(rsclient: &KanidmClient, id: &str, group_name: &str) {
|
|||
rsclient.idm_group_unix_extend(&group_name, None).unwrap();
|
||||
|
||||
// Extend with person to allow legalname
|
||||
rsclient.idm_account_person_extend(id).unwrap();
|
||||
rsclient.idm_account_person_extend(id, None, None).unwrap();
|
||||
|
||||
["ssh_publickey", "legalname", "mail"]
|
||||
.iter()
|
||||
|
|
|
@ -52,6 +52,19 @@ fn test_oauth2_openid_basic_flow() {
|
|||
)
|
||||
.expect("Failed to create oauth2 config");
|
||||
|
||||
// Extend the admin account with extended details for openid claims.
|
||||
rsclient
|
||||
.idm_group_add_members("idm_hp_people_extend_priv", &["admin"])
|
||||
.unwrap();
|
||||
|
||||
rsclient
|
||||
.idm_account_person_extend(
|
||||
"admin",
|
||||
Some(&["admin@example.com".to_string()]),
|
||||
Some("Admin Istrator"),
|
||||
)
|
||||
.expect("Failed to extend account details");
|
||||
|
||||
rsclient
|
||||
.idm_oauth2_rs_update(
|
||||
"test_integration",
|
||||
|
@ -78,6 +91,9 @@ fn test_oauth2_openid_basic_flow() {
|
|||
.expect("No basic secret present");
|
||||
|
||||
// Get our admin's auth token for our new client.
|
||||
// We have to re-auth to update the mail field.
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
let admin_uat = rsclient.get_token().expect("No user auth token found");
|
||||
|
||||
let url = rsclient.get_url().to_string();
|
||||
|
@ -313,6 +329,8 @@ fn test_oauth2_openid_basic_flow() {
|
|||
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
||||
// token and the userinfo endpoints.
|
||||
assert!(oidc.iss == Url::parse("https://idm.example.com/").unwrap());
|
||||
assert!(oidc.s_claims.email.as_deref() == Some("admin@example.com"));
|
||||
assert!(oidc.s_claims.email_verified == Some(true));
|
||||
|
||||
let response = client
|
||||
.get(format!("{}/oauth2/openid/test_integration/userinfo", url))
|
||||
|
|
|
@ -709,7 +709,9 @@ fn test_server_rest_account_import_password() {
|
|||
.unwrap();
|
||||
|
||||
// Make them a person, so we can import the password
|
||||
rsclient.idm_account_person_extend("demo_account").unwrap();
|
||||
rsclient
|
||||
.idm_account_person_extend("demo_account", None, None)
|
||||
.unwrap();
|
||||
|
||||
// Attempt to import a bad password
|
||||
let r = rsclient.idm_account_primary_credential_import_password("demo_account", "password");
|
||||
|
|
|
@ -202,6 +202,7 @@ pub struct UserAuthToken {
|
|||
// pub name: String,
|
||||
pub displayname: String,
|
||||
pub spn: String,
|
||||
pub mail_primary: Option<String>,
|
||||
// pub groups: Vec<Group>,
|
||||
// Should we just retrieve these inside the server instead of in the uat?
|
||||
// or do we want per-session limit capabilities?
|
||||
|
@ -319,6 +320,19 @@ pub struct AccountUnixExtend {
|
|||
pub shell: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AccountPersonExtend {
|
||||
pub mail: Option<Vec<String>>,
|
||||
pub legalname: Option<String>,
|
||||
}
|
||||
|
||||
/*
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AccountOrgPersonExtend {
|
||||
pub mail: String,
|
||||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub enum CredentialDetailType {
|
||||
Password,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::password_prompt;
|
||||
use crate::{
|
||||
AccountCredential, AccountOpt, AccountPosix, AccountRadius, AccountSsh, AccountValidity,
|
||||
AccountCredential, AccountOpt, AccountPerson, AccountPosix, AccountRadius, AccountSsh,
|
||||
AccountValidity,
|
||||
};
|
||||
use qrcode::render::unicode;
|
||||
use qrcode::QrCode;
|
||||
|
@ -37,6 +38,9 @@ impl AccountOpt {
|
|||
AccountPosix::Set(apo) => apo.copt.debug,
|
||||
AccountPosix::SetPassword(apo) => apo.copt.debug,
|
||||
},
|
||||
AccountOpt::Person(apopt) => match apopt {
|
||||
AccountPerson::Set(apo) => apo.copt.debug,
|
||||
},
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(ano) => ano.copt.debug,
|
||||
AccountSsh::Add(ano) => ano.copt.debug,
|
||||
|
@ -391,6 +395,18 @@ impl AccountOpt {
|
|||
}
|
||||
}
|
||||
}, // end AccountOpt::Posix
|
||||
AccountOpt::Person(apopt) => match apopt {
|
||||
AccountPerson::Set(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
if let Err(e) = client.idm_account_person_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.mail.as_deref(),
|
||||
aopt.legalname.as_deref(),
|
||||
) {
|
||||
eprintln!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}, // end AccountOpt::Person
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
|
|
|
@ -219,6 +219,24 @@ pub enum AccountPosix {
|
|||
SetPassword(AccountNamedOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AccountPersonOpt {
|
||||
#[structopt(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[structopt(long = "mail")]
|
||||
mail: Option<Vec<String>>,
|
||||
#[structopt(long = "legalname")]
|
||||
legalname: Option<String>,
|
||||
#[structopt(flatten)]
|
||||
copt: CommonOpt,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum AccountPerson {
|
||||
#[structopt(name = "set")]
|
||||
Set(AccountPersonOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum AccountSsh {
|
||||
#[structopt(name = "list_publickeys")]
|
||||
|
@ -247,6 +265,8 @@ pub enum AccountOpt {
|
|||
Radius(AccountRadius),
|
||||
#[structopt(name = "posix")]
|
||||
Posix(AccountPosix),
|
||||
#[structopt(name = "person")]
|
||||
Person(AccountPerson),
|
||||
#[structopt(name = "ssh")]
|
||||
Ssh(AccountSsh),
|
||||
#[structopt(name = "list")]
|
||||
|
|
|
@ -29,7 +29,7 @@ fn main() {
|
|||
}
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
debug!("Starting cache status tool ...");
|
||||
trace!("Starting cache status tool ...");
|
||||
|
||||
let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
{
|
||||
|
@ -51,7 +51,7 @@ fn main() {
|
|||
} else {
|
||||
match call_daemon_blocking(cfg.sock_path.as_str(), &req) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("working!"),
|
||||
ClientResponse::Ok => println!("working!"),
|
||||
_ => {
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ use kanidm_proto::v1::Entry as ProtoEntry;
|
|||
use kanidm_proto::v1::Modify as ProtoModify;
|
||||
use kanidm_proto::v1::ModifyList as ProtoModifyList;
|
||||
use kanidm_proto::v1::{
|
||||
AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest,
|
||||
SetCredentialRequest, SetCredentialResponse,
|
||||
AccountPersonExtend, AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend,
|
||||
ModifyRequest, SetCredentialRequest, SetCredentialResponse,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
@ -963,17 +963,43 @@ impl QueryServerWriteV1 {
|
|||
&self,
|
||||
uat: Option<String>,
|
||||
uuid_or_name: String,
|
||||
px: AccountPersonExtend,
|
||||
eventid: Uuid,
|
||||
) -> Result<(), OperationError> {
|
||||
let AccountPersonExtend { mail, legalname } = px;
|
||||
|
||||
// The filter_map here means we only create the mods if the gidnumber or shell are set
|
||||
// in the actual request.
|
||||
// NOTE: This is an iter for future requirements to be added
|
||||
let mods: Vec<_> = iter::once(Some(Modify::Present(
|
||||
"class".into(),
|
||||
Value::new_class("person"),
|
||||
)))
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut mods: Vec<_> = Vec::with_capacity(4 + mail.as_ref().map(|v| v.len()).unwrap_or(0));
|
||||
mods.push(Modify::Present("class".into(), Value::new_class("person")));
|
||||
|
||||
if let Some(s) = legalname {
|
||||
mods.push(Modify::Purged("legalname".into()));
|
||||
mods.push(Modify::Present("legalname".into(), Value::new_utf8(s)));
|
||||
}
|
||||
|
||||
if let Some(mail) = mail {
|
||||
mods.push(Modify::Purged("mail".into()));
|
||||
|
||||
let mut miter = mail.into_iter();
|
||||
if let Some(m_primary) = miter.next() {
|
||||
let v =
|
||||
Value::new_email_address_primary_s(m_primary.as_str()).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(format!(
|
||||
"Invalid mail address {}",
|
||||
m_primary
|
||||
))
|
||||
})?;
|
||||
mods.push(Modify::Present("mail".into(), v));
|
||||
}
|
||||
|
||||
for m in miter {
|
||||
let v = Value::new_email_address_s(m.as_str()).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(format!("Invalid mail address {}", m))
|
||||
})?;
|
||||
mods.push(Modify::Present("mail".into(), v));
|
||||
}
|
||||
}
|
||||
|
||||
let ml = ModifyList::new_list(mods);
|
||||
|
||||
|
|
|
@ -168,7 +168,7 @@ pub const JSON_IDM_ACP_PEOPLE_READ_PRIV_V1: &str = r#"{
|
|||
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000002\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
"{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
|
@ -278,8 +278,85 @@ pub const JSON_IDM_ACP_PEOPLE_EXTEND_PRIV_V1: &str = r#"{
|
|||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class"
|
||||
"class", "name", "displayname", "legalname", "mail"
|
||||
],
|
||||
"acp_modify_class": ["person"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// -- hp people
|
||||
pub const JSON_IDM_ACP_HP_PEOPLE_READ_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_search"
|
||||
],
|
||||
"name": ["idm_acp_hp_people_read_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000036"],
|
||||
"description": ["Builtin IDM Control for reading high privilege personal sensitive data."],
|
||||
"acp_receiver": [
|
||||
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000028\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_IDM_ACP_HP_PEOPLE_WRITE_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_modify"
|
||||
],
|
||||
"name": ["idm_acp_hp_people_write_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000037"],
|
||||
"description": ["Builtin IDM Control for managing privilege personal and sensitive data."],
|
||||
"acp_receiver": [
|
||||
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000029\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"person\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_IDM_ACP_HP_PEOPLE_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_modify"
|
||||
],
|
||||
"name": ["idm_acp_hp_people_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000038"],
|
||||
"description": ["Builtin IDM Control for allowing privilege person class extension"],
|
||||
"acp_receiver": [
|
||||
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000030\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "mail"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "name", "displayname", "legalname", "mail"
|
||||
],
|
||||
"acp_modify_class": ["person"]
|
||||
}
|
||||
|
@ -891,7 +968,7 @@ pub const JSON_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"class", "name", "spn", "uuid", "description", "gidnumber", "loginshell", "unix_password"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
"loginshell", "gidnumber", "unix_password"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
|
@ -921,7 +998,7 @@ pub const JSON_IDM_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"class", "name", "spn", "uuid", "description", "member", "gidnumber"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "gidnumber"
|
||||
"gidnumber"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "gidnumber"
|
||||
|
@ -952,7 +1029,7 @@ pub const JSON_IDM_HP_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"class", "name", "spn", "uuid", "description", "gidnumber", "loginshell", "unix_password"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
"loginshell", "gidnumber", "unix_password"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
|
@ -982,7 +1059,7 @@ pub const JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"class", "name", "spn", "uuid", "description", "member", "gidnumber"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "gidnumber"
|
||||
"gidnumber"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "gidnumber"
|
||||
|
|
|
@ -95,6 +95,38 @@ pub const JSON_IDM_PEOPLE_EXTEND_PRIV_V1: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_IDM_HP_PEOPLE_READ_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_hp_people_read_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000028"],
|
||||
"description": ["Builtin IDM Group for granting elevated high privilege people (personal data) read permissions."],
|
||||
"member": ["00000000-0000-0000-0000-000000000029"]
|
||||
}
|
||||
}"#;
|
||||
pub const JSON_IDM_HP_PEOPLE_WRITE_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_hp_people_write_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000029"],
|
||||
"description": ["Builtin IDM Group for granting elevated high privilege people (personal data) write permissions."],
|
||||
"member": [
|
||||
"00000000-0000-0000-0000-000000000030"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
pub const JSON_IDM_HP_PEOPLE_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_hp_people_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000030"],
|
||||
"description": ["Builtin IDM Group for extending high privilege accounts to be people."],
|
||||
"member": [
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// * group write manager (no read, everyone has read via the anon, etc)
|
||||
// IDM_GROUP_CREATE_PRIV
|
||||
pub const JSON_IDM_GROUP_MANAGE_PRIV_V1: &str = r#"{
|
||||
|
|
|
@ -837,6 +837,35 @@ pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
|||
}
|
||||
"#;
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_ORGPERSON: &str = r#"
|
||||
{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"classtype"
|
||||
],
|
||||
"description": [
|
||||
"Object representation of an org person"
|
||||
],
|
||||
"classname": [
|
||||
"orgperson"
|
||||
],
|
||||
"systemmay": [
|
||||
"legalname"
|
||||
],
|
||||
"systemmust": [
|
||||
"mail",
|
||||
"displayname",
|
||||
"name"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000094"
|
||||
]
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_GROUP: &str = r#"
|
||||
{
|
||||
"attrs": {
|
||||
|
|
|
@ -30,12 +30,13 @@ pub const _UUID_IDM_GROUP_UNIX_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-000
|
|||
pub const _UUID_IDM_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-000000000023");
|
||||
pub const _UUID_IDM_PEOPLE_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000024");
|
||||
|
||||
pub const _UUID_IDM_HP_ACCOUNT_UNIX_EXTEND_PRIV: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-000000000025");
|
||||
pub const _UUID_IDM_HP_GROUP_UNIX_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000026");
|
||||
|
||||
pub const _UUID_IDM_HP_OAUTH2_MANAGE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000027");
|
||||
pub const _UUID_IDM_HP_PEOPLE_READ_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000028");
|
||||
pub const _UUID_IDM_HP_PEOPLE_WRITE_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000029");
|
||||
pub const _UUID_IDM_HP_PEOPLE_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000030");
|
||||
|
||||
//
|
||||
pub const _UUID_IDM_HIGH_PRIVILEGE: Uuid = uuid!("00000000-0000-0000-0000-000000001000");
|
||||
|
@ -153,6 +154,7 @@ pub const _UUID_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffff00000092");
|
||||
pub const _UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffff00000093");
|
||||
pub const _UUID_SCHEMA_CLASS_ORGPERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000094");
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for making poor choices about these allocations.
|
||||
|
@ -211,6 +213,12 @@ pub const _UUID_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1: Uuid =
|
|||
uuid!("00000000-0000-0000-0000-ffffff000034");
|
||||
pub const _UUID_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffffff000035");
|
||||
pub const _UUID_IDM_ACP_HP_PEOPLE_READ_PRIV_V1: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffffff000036");
|
||||
pub const _UUID_IDM_ACP_HP_PEOPLE_WRITE_PRIV_V1: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffffff000037");
|
||||
pub const _UUID_IDM_ACP_HP_PEOPLE_EXTEND_PRIV_V1: Uuid =
|
||||
uuid!("00000000-0000-0000-0000-ffffff000038");
|
||||
|
||||
// End of system ranges
|
||||
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::status::StatusRequestEvent;
|
|||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState, CreateRequest,
|
||||
DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest,
|
||||
AccountPersonExtend, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
|
||||
CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest,
|
||||
SetCredentialRequest, SingleStringRequest,
|
||||
};
|
||||
|
||||
|
@ -347,7 +347,7 @@ pub async fn person_get(req: tide::Request<AppState>) -> tide::Result {
|
|||
}
|
||||
|
||||
pub async fn person_post(req: tide::Request<AppState>) -> tide::Result {
|
||||
let classes = vec!["account".to_string(), "object".to_string()];
|
||||
let classes = vec!["person".to_string(), "object".to_string()];
|
||||
json_rest_event_post(req, classes).await
|
||||
}
|
||||
|
||||
|
@ -545,14 +545,15 @@ pub async fn account_get_id_radius_token(req: tide::Request<AppState>) -> tide::
|
|||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
||||
pub async fn account_post_id_person_extend(req: tide::Request<AppState>) -> tide::Result {
|
||||
pub async fn account_post_id_person_extend(mut req: tide::Request<AppState>) -> tide::Result {
|
||||
let uat = req.get_current_uat();
|
||||
let uuid_or_name = req.get_url_param("id")?;
|
||||
let obj: AccountPersonExtend = req.body_json().await?;
|
||||
let (eventid, hvalue) = req.new_eventid();
|
||||
let res = req
|
||||
.state()
|
||||
.qe_w_ref
|
||||
.handle_idmaccountpersonextend(uat, uuid_or_name, eventid)
|
||||
.handle_idmaccountpersonextend(uat, uuid_or_name, obj, eventid)
|
||||
.await;
|
||||
to_tide_response(res, hvalue)
|
||||
}
|
||||
|
|
|
@ -1714,6 +1714,16 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
self.attrs.get(attr).and_then(|vs| vs.to_refer_single())
|
||||
}
|
||||
|
||||
pub fn get_ava_mail_primary(&self, attr: &str) -> Option<&str> {
|
||||
self.attrs
|
||||
.get(attr)
|
||||
.and_then(|vs| vs.to_email_address_primary_str())
|
||||
}
|
||||
|
||||
pub fn get_ava_iter_mail(&self, attr: &str) -> Option<impl Iterator<Item = &str>> {
|
||||
self.get_ava_set(attr).and_then(|vs| vs.as_email_str_iter())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
/// Return a single protocol filter, if valid to transform this value.
|
||||
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {
|
||||
|
|
|
@ -56,6 +56,13 @@ macro_rules! try_from_entry {
|
|||
OperationError::InvalidAccountState("Missing attribute: spn".to_string()),
|
||||
)?;
|
||||
|
||||
let mail_primary = $value.get_ava_mail_primary("mail").map(str::to_string);
|
||||
|
||||
let mail = $value
|
||||
.get_ava_iter_mail("mail")
|
||||
.map(|i| i.map(str::to_string).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
@ -79,6 +86,8 @@ macro_rules! try_from_entry {
|
|||
expire,
|
||||
radius_secret,
|
||||
spn,
|
||||
mail_primary,
|
||||
mail,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
@ -107,7 +116,8 @@ pub(crate) struct Account {
|
|||
pub spn: String,
|
||||
// TODO #256: When you add mail, you should update the check to zxcvbn
|
||||
// to include these.
|
||||
// pub mail: Vec<String>
|
||||
pub mail_primary: Option<String>,
|
||||
pub mail: Vec<String>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
|
@ -167,14 +177,15 @@ impl Account {
|
|||
|
||||
Some(UserAuthToken {
|
||||
session_id,
|
||||
auth_type,
|
||||
expiry,
|
||||
// name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
displayname: self.displayname.clone(),
|
||||
uuid: self.uuid,
|
||||
// name: self.name.clone(),
|
||||
displayname: self.displayname.clone(),
|
||||
spn: self.spn.clone(),
|
||||
mail_primary: self.mail_primary.clone(),
|
||||
// application: None,
|
||||
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
|
||||
auth_type,
|
||||
// What's the best way to get access to these limits with regard to claims/other?
|
||||
lim_uidx: false,
|
||||
lim_rmax: 128,
|
||||
|
@ -212,6 +223,21 @@ impl Account {
|
|||
Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref())
|
||||
}
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
pub fn related_inputs(&self) -> Vec<&str> {
|
||||
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
||||
self.mail.iter().for_each(|m| {
|
||||
inputs.push(m.as_str());
|
||||
});
|
||||
inputs.push(self.name.as_str());
|
||||
inputs.push(self.spn.as_str());
|
||||
inputs.push(self.displayname.as_str());
|
||||
if let Some(s) = self.radius_secret.as_deref() {
|
||||
inputs.push(s);
|
||||
}
|
||||
inputs
|
||||
}
|
||||
|
||||
pub fn primary_cred_uuid(&self) -> Uuid {
|
||||
match &self.primary {
|
||||
Some(cred) => cred.uuid,
|
||||
|
|
|
@ -818,6 +818,16 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
// TODO: Can the user consent to which claims are released? Today as we don't support most
|
||||
// of them anyway, no, but in the future, we can stash these to the consent req.
|
||||
|
||||
let (email, email_verified) = if scope_set.contains("email") {
|
||||
if let Some(mp) = code_xchg.uat.mail_primary {
|
||||
(Some(mp.to_string()), Some(true))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// TODO: If max_age was requested in the request, we MUST provide auth_time.
|
||||
|
||||
// amr == auth method
|
||||
|
@ -846,6 +856,8 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
// Map from spn
|
||||
preferred_username: Some(code_xchg.uat.spn.clone()),
|
||||
scopes: code_xchg.scopes.clone(),
|
||||
email,
|
||||
email_verified,
|
||||
..Default::default()
|
||||
},
|
||||
claims: Default::default(),
|
||||
|
@ -1034,6 +1046,16 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
}
|
||||
};
|
||||
|
||||
let (email, email_verified) = if at.scopes.contains(&"email".to_string()) {
|
||||
if let Some(mp) = account.mail_primary {
|
||||
(Some(mp.to_string()), Some(true))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let amr = Some(vec![at.auth_type.to_string()]);
|
||||
|
||||
// ==== good to generate response ====
|
||||
|
@ -1058,6 +1080,8 @@ impl Oauth2ResourceServersReadTransaction {
|
|||
// Map from spn
|
||||
preferred_username: Some(account.spn),
|
||||
scopes: at.scopes,
|
||||
email,
|
||||
email_verified,
|
||||
..Default::default()
|
||||
},
|
||||
claims: Default::default(),
|
||||
|
|
|
@ -1283,18 +1283,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// Check the password quality.
|
||||
// Ask if tis all good - this step checks pwpolicy and such
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
let mut related_inputs: Vec<&str> = vec![
|
||||
account.name.as_str(),
|
||||
account.displayname.as_str(),
|
||||
account.spn.as_str(),
|
||||
];
|
||||
|
||||
if let Some(s) = account.radius_secret.as_ref() {
|
||||
related_inputs.push(s.as_str())
|
||||
};
|
||||
|
||||
self.check_password_quality(pce.cleartext.as_str(), related_inputs.as_slice())
|
||||
self.check_password_quality(pce.cleartext.as_str(), account.related_inputs().as_slice())
|
||||
.map_err(|e| {
|
||||
request_error!(err = ?e, "check_password_quality");
|
||||
e
|
||||
|
@ -1370,18 +1360,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
// If we got here, then pre-apply succedded, and that means access control
|
||||
// passed. Now we can do the extra checks.
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
let mut related_inputs: Vec<&str> = vec![
|
||||
account.name.as_str(),
|
||||
account.displayname.as_str(),
|
||||
account.spn.as_str(),
|
||||
];
|
||||
|
||||
if let Some(s) = account.radius_secret.as_ref() {
|
||||
related_inputs.push(s.as_str())
|
||||
};
|
||||
|
||||
self.check_password_quality(pce.cleartext.as_str(), related_inputs.as_slice())
|
||||
self.check_password_quality(pce.cleartext.as_str(), account.related_inputs().as_slice())
|
||||
.map_err(|e| {
|
||||
admin_error!(?e, "Failed to checked password quality");
|
||||
e
|
||||
|
|
|
@ -30,6 +30,7 @@ pub(crate) struct UnixUserAccount {
|
|||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
pub radius_secret: Option<String>,
|
||||
pub mail: Vec<String>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
@ -94,6 +95,11 @@ macro_rules! try_from_entry {
|
|||
.get_ava_single_secret("radius_secret")
|
||||
.map(str::to_string);
|
||||
|
||||
let mail = $value
|
||||
.get_ava_iter_mail("mail")
|
||||
.map(|i| i.map(str::to_string).collect())
|
||||
.unwrap_or_else(Vec::new);
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
@ -111,6 +117,7 @@ macro_rules! try_from_entry {
|
|||
valid_from,
|
||||
expire,
|
||||
radius_secret,
|
||||
mail,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
@ -202,6 +209,21 @@ impl UnixUserAccount {
|
|||
vmin && vmax
|
||||
}
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
pub fn related_inputs(&self) -> Vec<&str> {
|
||||
let mut inputs = Vec::with_capacity(4 + self.mail.len());
|
||||
self.mail.iter().for_each(|m| {
|
||||
inputs.push(m.as_str());
|
||||
});
|
||||
inputs.push(self.name.as_str());
|
||||
inputs.push(self.spn.as_str());
|
||||
inputs.push(self.displayname.as_str());
|
||||
if let Some(s) = self.radius_secret.as_deref() {
|
||||
inputs.push(s);
|
||||
}
|
||||
inputs
|
||||
}
|
||||
|
||||
pub(crate) fn verify_unix_credential(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
|
|
|
@ -2225,6 +2225,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
|
||||
JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_ORGPERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||
JSON_SCHEMA_CLASS_DOMAIN_INFO,
|
||||
|
@ -2302,6 +2303,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_PEOPLE_EXTEND_PRIV_V1,
|
||||
JSON_IDM_PEOPLE_WRITE_PRIV_V1,
|
||||
JSON_IDM_PEOPLE_READ_PRIV_V1,
|
||||
JSON_IDM_HP_PEOPLE_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_PEOPLE_WRITE_PRIV_V1,
|
||||
JSON_IDM_HP_PEOPLE_READ_PRIV_V1,
|
||||
JSON_IDM_GROUP_MANAGE_PRIV_V1,
|
||||
JSON_IDM_GROUP_WRITE_PRIV_V1,
|
||||
JSON_IDM_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
|
@ -2354,6 +2358,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_ACP_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACP_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1,
|
||||
JSON_IDM_ACP_PEOPLE_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACP_HP_PEOPLE_READ_PRIV_V1,
|
||||
JSON_IDM_ACP_HP_PEOPLE_WRITE_PRIV_V1,
|
||||
JSON_IDM_ACP_HP_PEOPLE_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1,
|
||||
|
|
|
@ -17,8 +17,8 @@ use tracing::span::{Attributes, Record};
|
|||
use tracing::{Event, Id, Level, Metadata, Subscriber};
|
||||
use tracing_subscriber::layer::{Context, Layered, SubscriberExt};
|
||||
use tracing_subscriber::registry::{LookupSpan, Registry, Scope, SpanRef};
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::Layer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::tracing_tree::processor::TestProcessor;
|
||||
|
|
|
@ -110,7 +110,9 @@ impl From<Value> for ValueSet {
|
|||
Value::Nsuniqueid(s) => I::Nsuniqueid(btreeset![s]),
|
||||
Value::DateTime(dt) => I::DateTime(smolset![dt]),
|
||||
Value::EmailAddress(e, _) => I::EmailAddress {
|
||||
primary: None,
|
||||
// We have to disregard the primary here
|
||||
// as we only have one!
|
||||
primary: Some(e.clone()),
|
||||
set: btreeset![e],
|
||||
},
|
||||
Value::Url(u) => I::Url(smolset![u]),
|
||||
|
@ -179,13 +181,10 @@ impl TryFrom<DbValueV1> for ValueSet {
|
|||
}
|
||||
DbValueV1::EmailAddress(DbValueEmailAddressV1 {
|
||||
d: email_addr,
|
||||
p: is_primary,
|
||||
p: _,
|
||||
}) => {
|
||||
let primary = if is_primary {
|
||||
Some(email_addr.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Since this is the first, we need to disregard the primary.
|
||||
let primary = Some(email_addr.clone());
|
||||
I::EmailAddress {
|
||||
primary,
|
||||
set: btreeset![email_addr],
|
||||
|
@ -377,7 +376,6 @@ impl ValueSet {
|
|||
}),
|
||||
) => {
|
||||
if is_primary {
|
||||
debug_assert!(primary.is_none());
|
||||
*primary = Some(email_addr.clone());
|
||||
};
|
||||
|
||||
|
@ -391,7 +389,6 @@ impl ValueSet {
|
|||
}),
|
||||
) => {
|
||||
if is_primary {
|
||||
debug_assert!(primary.is_none());
|
||||
*primary = Some(phone_number.clone());
|
||||
};
|
||||
Ok(set.insert(phone_number))
|
||||
|
@ -711,10 +708,12 @@ impl ValueSet {
|
|||
I::DateTime(set) => {
|
||||
set.clear();
|
||||
}
|
||||
I::EmailAddress { primary: _, set } => {
|
||||
I::EmailAddress { primary, set } => {
|
||||
*primary = None;
|
||||
set.clear();
|
||||
}
|
||||
I::PhoneNumber { primary: _, set } => {
|
||||
I::PhoneNumber { primary, set } => {
|
||||
*primary = None;
|
||||
set.clear();
|
||||
}
|
||||
I::Address { set } => {
|
||||
|
@ -797,10 +796,11 @@ impl ValueSet {
|
|||
set.remove(dt);
|
||||
}
|
||||
(I::EmailAddress { primary, set }, PartialValue::EmailAddress(e)) => {
|
||||
if Some(e) == primary.as_ref() {
|
||||
*primary = None;
|
||||
};
|
||||
set.remove(e);
|
||||
if Some(e) == primary.as_ref() {
|
||||
*primary = set.iter().cloned().next();
|
||||
// *primary = None;
|
||||
};
|
||||
}
|
||||
(I::PhoneNumber { primary, set }, PartialValue::PhoneNumber(e)) => {
|
||||
if Some(e) == primary.as_ref() {
|
||||
|
@ -1291,6 +1291,13 @@ impl ValueSet {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn as_email_set(&self) -> Option<&BTreeSet<String>> {
|
||||
match &self.inner {
|
||||
I::EmailAddress { primary: _, set } => Some(set),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_sshkey_map(&self) -> Option<&BTreeMap<String, String>> {
|
||||
match &self.inner {
|
||||
I::SshKey(map) => Some(map),
|
||||
|
@ -1522,6 +1529,13 @@ impl ValueSet {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn as_email_str_iter(&self) -> Option<impl Iterator<Item = &str>> {
|
||||
match &self.inner {
|
||||
I::EmailAddress { primary: _, set } => Some(set.iter().map(|s| s.as_str())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_oauthscope_iter(&self) -> Option<impl Iterator<Item = &str>> {
|
||||
match &self.inner {
|
||||
I::OauthScope(set) => Some(set.iter().map(|s| s.as_str())),
|
||||
|
@ -2368,7 +2382,7 @@ mod tests {
|
|||
ValueSet::new(Value::new_email_address_s("claire@example.com").expect("Invalid Email"));
|
||||
|
||||
assert!(vs.len() == 1);
|
||||
assert!(vs.to_email_address_primary_str().is_none());
|
||||
assert!(vs.to_email_address_primary_str() == Some("claire@example.com"));
|
||||
|
||||
// Add another, still not primary.
|
||||
assert!(
|
||||
|
@ -2378,7 +2392,7 @@ mod tests {
|
|||
);
|
||||
|
||||
assert!(vs.len() == 2);
|
||||
assert!(vs.to_email_address_primary_str().is_none());
|
||||
assert!(vs.to_email_address_primary_str() == Some("claire@example.com"));
|
||||
|
||||
// Update primary
|
||||
assert!(
|
||||
|
@ -2396,17 +2410,22 @@ mod tests {
|
|||
assert!(vs == vs2);
|
||||
assert!(vs.to_email_address_primary_str() == vs2.to_email_address_primary_str());
|
||||
|
||||
// Remove primary, assert it's gone.
|
||||
// Remove primary, assert it's gone and that the "first" address is assigned.
|
||||
assert!(vs.remove(&PartialValue::new_email_address_s("primary@example.com")));
|
||||
assert!(vs.len() == 2);
|
||||
assert!(vs.to_email_address_primary_str().is_none());
|
||||
|
||||
// Restore form dbv1, no primary.
|
||||
assert!(vs.to_email_address_primary_str() == Some("alice@example.com"));
|
||||
|
||||
// Restore from dbv1, alice persisted.
|
||||
let vs3 = ValueSet::from_db_valuev1_iter(vs.to_db_valuev1_iter())
|
||||
.expect("Failed to construct vs2 from dbvalue");
|
||||
assert!(vs == vs3);
|
||||
assert!(vs.to_email_address_primary_str() != vs2.to_email_address_primary_str());
|
||||
assert!(vs.to_email_address_primary_str() == vs3.to_email_address_primary_str());
|
||||
assert!(vs3.len() == 2);
|
||||
assert!(vs3.as_email_set().unwrap().contains("alice@example.com"));
|
||||
assert!(vs3.as_email_set().unwrap().contains("claire@example.com"));
|
||||
|
||||
// If we clear, no primary.
|
||||
vs.clear();
|
||||
assert!(vs.len() == 0);
|
||||
assert!(vs.to_email_address_primary_str().is_none());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue