Finalise email changes for oidc (#629)

This commit is contained in:
Firstyear 2021-12-25 09:47:14 +10:00 committed by GitHub
parent dc1dd11333
commit c6c564cebb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 456 additions and 84 deletions

View file

@ -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* > scope map OR implicit scope named 'openid'. Without this, openid clients *WILL NOT WORK*
> **HINT** > **HINT**
> openid connect provides a number of scopes that affect the content of the resulting > openid connect allows a number of scopes that affect the content of the resulting
> authorisation token. Supported scopes and their associated claims are: > 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) > * 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) > * email - (email, email\_verified)
> * address - (address) > * address - (address)
> * phone - (phone\_number, phone\_number\_verified) > * phone - (phone\_number, phone\_number\_verified)
>
Once created you can view the details of the resource server. Once created you can view the details of the resource server.

View file

@ -1278,6 +1278,20 @@ impl KanidmAsyncClient {
.await .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> { 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()) self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str())
.await .await
@ -1294,8 +1308,17 @@ impl KanidmAsyncClient {
.await .await
} }
pub async fn idm_account_person_extend(&self, id: &str) -> Result<(), ClientError> { pub async fn idm_account_person_extend(
self.perform_post_request(format!("/v1/account/{}/_person/_extend", id).as_str(), ()) &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 .await
} }

View file

@ -810,6 +810,16 @@ impl KanidmClient {
tokio_block_on(self.asclient.idm_account_unix_cred_verify(id, cred)) 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> { 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)) 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)) tokio_block_on(self.asclient.idm_account_post_ssh_pubkey(id, tag, pubkey))
} }
pub fn idm_account_person_extend(&self, id: &str) -> Result<(), ClientError> { pub fn idm_account_person_extend(
tokio_block_on(self.asclient.idm_account_person_extend(id)) &self,
id: &str,
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_account_person_extend(id, mail, legalname))
} }
/* /*

View file

@ -123,7 +123,7 @@ fn add_all_attrs(rsclient: &KanidmClient, id: &str, group_name: &str) {
rsclient.idm_group_unix_extend(&group_name, None).unwrap(); rsclient.idm_group_unix_extend(&group_name, None).unwrap();
// Extend with person to allow legalname // 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"] ["ssh_publickey", "legalname", "mail"]
.iter() .iter()

View file

@ -52,6 +52,19 @@ fn test_oauth2_openid_basic_flow() {
) )
.expect("Failed to create oauth2 config"); .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 rsclient
.idm_oauth2_rs_update( .idm_oauth2_rs_update(
"test_integration", "test_integration",
@ -78,6 +91,9 @@ fn test_oauth2_openid_basic_flow() {
.expect("No basic secret present"); .expect("No basic secret present");
// Get our admin's auth token for our new client. // 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 admin_uat = rsclient.get_token().expect("No user auth token found");
let url = rsclient.get_url().to_string(); 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 // This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
// token and the userinfo endpoints. // token and the userinfo endpoints.
assert!(oidc.iss == Url::parse("https://idm.example.com/").unwrap()); 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 let response = client
.get(format!("{}/oauth2/openid/test_integration/userinfo", url)) .get(format!("{}/oauth2/openid/test_integration/userinfo", url))

View file

@ -709,7 +709,9 @@ fn test_server_rest_account_import_password() {
.unwrap(); .unwrap();
// Make them a person, so we can import the password // 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 // Attempt to import a bad password
let r = rsclient.idm_account_primary_credential_import_password("demo_account", "password"); let r = rsclient.idm_account_primary_credential_import_password("demo_account", "password");

View file

@ -202,6 +202,7 @@ pub struct UserAuthToken {
// pub name: String, // pub name: String,
pub displayname: String, pub displayname: String,
pub spn: String, pub spn: String,
pub mail_primary: Option<String>,
// pub groups: Vec<Group>, // pub groups: Vec<Group>,
// Should we just retrieve these inside the server instead of in the uat? // Should we just retrieve these inside the server instead of in the uat?
// or do we want per-session limit capabilities? // or do we want per-session limit capabilities?
@ -319,6 +320,19 @@ pub struct AccountUnixExtend {
pub shell: Option<String>, 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)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum CredentialDetailType { pub enum CredentialDetailType {
Password, Password,

View file

@ -1,6 +1,7 @@
use crate::password_prompt; use crate::password_prompt;
use crate::{ use crate::{
AccountCredential, AccountOpt, AccountPosix, AccountRadius, AccountSsh, AccountValidity, AccountCredential, AccountOpt, AccountPerson, AccountPosix, AccountRadius, AccountSsh,
AccountValidity,
}; };
use qrcode::render::unicode; use qrcode::render::unicode;
use qrcode::QrCode; use qrcode::QrCode;
@ -37,6 +38,9 @@ impl AccountOpt {
AccountPosix::Set(apo) => apo.copt.debug, AccountPosix::Set(apo) => apo.copt.debug,
AccountPosix::SetPassword(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 { AccountOpt::Ssh(asopt) => match asopt {
AccountSsh::List(ano) => ano.copt.debug, AccountSsh::List(ano) => ano.copt.debug,
AccountSsh::Add(ano) => ano.copt.debug, AccountSsh::Add(ano) => ano.copt.debug,
@ -391,6 +395,18 @@ impl AccountOpt {
} }
} }
}, // end AccountOpt::Posix }, // 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 { AccountOpt::Ssh(asopt) => match asopt {
AccountSsh::List(aopt) => { AccountSsh::List(aopt) => {
let client = aopt.copt.to_client(); let client = aopt.copt.to_client();

View file

@ -219,6 +219,24 @@ pub enum AccountPosix {
SetPassword(AccountNamedOpt), 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)] #[derive(Debug, StructOpt)]
pub enum AccountSsh { pub enum AccountSsh {
#[structopt(name = "list_publickeys")] #[structopt(name = "list_publickeys")]
@ -247,6 +265,8 @@ pub enum AccountOpt {
Radius(AccountRadius), Radius(AccountRadius),
#[structopt(name = "posix")] #[structopt(name = "posix")]
Posix(AccountPosix), Posix(AccountPosix),
#[structopt(name = "person")]
Person(AccountPerson),
#[structopt(name = "ssh")] #[structopt(name = "ssh")]
Ssh(AccountSsh), Ssh(AccountSsh),
#[structopt(name = "list")] #[structopt(name = "list")]

View file

@ -29,7 +29,7 @@ fn main() {
} }
tracing_subscriber::fmt::init(); 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") let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config("/etc/kanidm/unixd")
{ {
@ -51,7 +51,7 @@ fn main() {
} else { } else {
match call_daemon_blocking(cfg.sock_path.as_str(), &req) { match call_daemon_blocking(cfg.sock_path.as_str(), &req) {
Ok(r) => match r { Ok(r) => match r {
ClientResponse::Ok => info!("working!"), ClientResponse::Ok => println!("working!"),
_ => { _ => {
error!("Error: unexpected response -> {:?}", r); error!("Error: unexpected response -> {:?}", r);
} }

View file

@ -28,8 +28,8 @@ use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::Modify as ProtoModify; use kanidm_proto::v1::Modify as ProtoModify;
use kanidm_proto::v1::ModifyList as ProtoModifyList; use kanidm_proto::v1::ModifyList as ProtoModifyList;
use kanidm_proto::v1::{ use kanidm_proto::v1::{
AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest, AccountPersonExtend, AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend,
SetCredentialRequest, SetCredentialResponse, ModifyRequest, SetCredentialRequest, SetCredentialResponse,
}; };
use uuid::Uuid; use uuid::Uuid;
@ -963,17 +963,43 @@ impl QueryServerWriteV1 {
&self, &self,
uat: Option<String>, uat: Option<String>,
uuid_or_name: String, uuid_or_name: String,
px: AccountPersonExtend,
eventid: Uuid, eventid: Uuid,
) -> Result<(), OperationError> { ) -> Result<(), OperationError> {
let AccountPersonExtend { mail, legalname } = px;
// The filter_map here means we only create the mods if the gidnumber or shell are set // The filter_map here means we only create the mods if the gidnumber or shell are set
// in the actual request. // in the actual request.
// NOTE: This is an iter for future requirements to be added let mut mods: Vec<_> = Vec::with_capacity(4 + mail.as_ref().map(|v| v.len()).unwrap_or(0));
let mods: Vec<_> = iter::once(Some(Modify::Present( mods.push(Modify::Present("class".into(), Value::new_class("person")));
"class".into(),
Value::new_class("person"), if let Some(s) = legalname {
))) mods.push(Modify::Purged("legalname".into()));
.flatten() mods.push(Modify::Present("legalname".into(), Value::new_utf8(s)));
.collect(); }
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); let ml = ModifyList::new_list(mods);

View file

@ -168,7 +168,7 @@ pub const JSON_IDM_ACP_PEOPLE_READ_PRIV_V1: &str = r#"{
"{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000002\"]}" "{\"eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000002\"]}"
], ],
"acp_targetscope": [ "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": [ "acp_search_attr": [
"name", "displayname", "legalname", "mail" "name", "displayname", "legalname", "mail"
@ -278,8 +278,85 @@ pub const JSON_IDM_ACP_PEOPLE_EXTEND_PRIV_V1: &str = r#"{
"acp_targetscope": [ "acp_targetscope": [
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}" "{\"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": [ "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"] "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" "class", "name", "spn", "uuid", "description", "gidnumber", "loginshell", "unix_password"
], ],
"acp_modify_removedattr": [ "acp_modify_removedattr": [
"class", "loginshell", "gidnumber", "unix_password" "loginshell", "gidnumber", "unix_password"
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"class", "loginshell", "gidnumber", "unix_password" "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" "class", "name", "spn", "uuid", "description", "member", "gidnumber"
], ],
"acp_modify_removedattr": [ "acp_modify_removedattr": [
"class", "gidnumber" "gidnumber"
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"class", "gidnumber" "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" "class", "name", "spn", "uuid", "description", "gidnumber", "loginshell", "unix_password"
], ],
"acp_modify_removedattr": [ "acp_modify_removedattr": [
"class", "loginshell", "gidnumber", "unix_password" "loginshell", "gidnumber", "unix_password"
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"class", "loginshell", "gidnumber", "unix_password" "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" "class", "name", "spn", "uuid", "description", "member", "gidnumber"
], ],
"acp_modify_removedattr": [ "acp_modify_removedattr": [
"class", "gidnumber" "gidnumber"
], ],
"acp_modify_presentattr": [ "acp_modify_presentattr": [
"class", "gidnumber" "class", "gidnumber"

View file

@ -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) // * group write manager (no read, everyone has read via the anon, etc)
// IDM_GROUP_CREATE_PRIV // IDM_GROUP_CREATE_PRIV
pub const JSON_IDM_GROUP_MANAGE_PRIV_V1: &str = r#"{ pub const JSON_IDM_GROUP_MANAGE_PRIV_V1: &str = r#"{

View file

@ -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#" pub const JSON_SCHEMA_CLASS_GROUP: &str = r#"
{ {
"attrs": { "attrs": {

View file

@ -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 = pub const _UUID_IDM_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000023"); uuid!("00000000-0000-0000-0000-000000000023");
pub const _UUID_IDM_PEOPLE_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000024"); pub const _UUID_IDM_PEOPLE_EXTEND_PRIV: Uuid = uuid!("00000000-0000-0000-0000-000000000024");
pub const _UUID_IDM_HP_ACCOUNT_UNIX_EXTEND_PRIV: Uuid = pub const _UUID_IDM_HP_ACCOUNT_UNIX_EXTEND_PRIV: Uuid =
uuid!("00000000-0000-0000-0000-000000000025"); 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_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_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"); 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"); uuid!("00000000-0000-0000-0000-ffff00000092");
pub const _UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: Uuid = pub const _UUID_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000093"); uuid!("00000000-0000-0000-0000-ffff00000093");
pub const _UUID_SCHEMA_CLASS_ORGPERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000094");
// System and domain infos // System and domain infos
// I'd like to strongly criticise william of the past for making poor choices about these allocations. // 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"); uuid!("00000000-0000-0000-0000-ffffff000034");
pub const _UUID_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: Uuid = pub const _UUID_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1: Uuid =
uuid!("00000000-0000-0000-0000-ffffff000035"); 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 // End of system ranges
pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe"); pub const UUID_DOES_NOT_EXIST: Uuid = uuid!("00000000-0000-0000-0000-fffffffffffe");

View file

@ -6,8 +6,8 @@ use crate::status::StatusRequestEvent;
use kanidm_proto::v1::Entry as ProtoEntry; use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::{ use kanidm_proto::v1::{
AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState, CreateRequest, AccountPersonExtend, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest,
SetCredentialRequest, SingleStringRequest, 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 { 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 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) 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 uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?; let uuid_or_name = req.get_url_param("id")?;
let obj: AccountPersonExtend = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid(); let (eventid, hvalue) = req.new_eventid();
let res = req let res = req
.state() .state()
.qe_w_ref .qe_w_ref
.handle_idmaccountpersonextend(uat, uuid_or_name, eventid) .handle_idmaccountpersonextend(uat, uuid_or_name, obj, eventid)
.await; .await;
to_tide_response(res, hvalue) to_tide_response(res, hvalue)
} }

View file

@ -1714,6 +1714,16 @@ impl<VALID, STATE> Entry<VALID, STATE> {
self.attrs.get(attr).and_then(|vs| vs.to_refer_single()) 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)] #[inline(always)]
/// Return a single protocol filter, if valid to transform this value. /// Return a single protocol filter, if valid to transform this value.
pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> { pub fn get_ava_single_protofilter(&self, attr: &str) -> Option<&ProtoFilter> {

View file

@ -56,6 +56,13 @@ macro_rules! try_from_entry {
OperationError::InvalidAccountState("Missing attribute: spn".to_string()), 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 valid_from = $value.get_ava_single_datetime("account_valid_from");
let expire = $value.get_ava_single_datetime("account_expire"); let expire = $value.get_ava_single_datetime("account_expire");
@ -79,6 +86,8 @@ macro_rules! try_from_entry {
expire, expire,
radius_secret, radius_secret,
spn, spn,
mail_primary,
mail,
}) })
}}; }};
} }
@ -107,7 +116,8 @@ pub(crate) struct Account {
pub spn: String, pub spn: String,
// TODO #256: When you add mail, you should update the check to zxcvbn // TODO #256: When you add mail, you should update the check to zxcvbn
// to include these. // to include these.
// pub mail: Vec<String> pub mail_primary: Option<String>,
pub mail: Vec<String>,
} }
impl Account { impl Account {
@ -167,14 +177,15 @@ impl Account {
Some(UserAuthToken { Some(UserAuthToken {
session_id, session_id,
auth_type,
expiry, expiry,
// name: self.name.clone(),
spn: self.spn.clone(),
displayname: self.displayname.clone(),
uuid: self.uuid, uuid: self.uuid,
// name: self.name.clone(),
displayname: self.displayname.clone(),
spn: self.spn.clone(),
mail_primary: self.mail_primary.clone(),
// application: None, // application: None,
// groups: self.groups.iter().map(|g| g.to_proto()).collect(), // 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? // What's the best way to get access to these limits with regard to claims/other?
lim_uidx: false, lim_uidx: false,
lim_rmax: 128, lim_rmax: 128,
@ -212,6 +223,21 @@ impl Account {
Self::check_within_valid_time(ct, self.valid_from.as_ref(), self.expire.as_ref()) 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 { pub fn primary_cred_uuid(&self) -> Uuid {
match &self.primary { match &self.primary {
Some(cred) => cred.uuid, Some(cred) => cred.uuid,

View file

@ -818,6 +818,16 @@ impl Oauth2ResourceServersReadTransaction {
// TODO: Can the user consent to which claims are released? Today as we don't support most // 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. // 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. // TODO: If max_age was requested in the request, we MUST provide auth_time.
// amr == auth method // amr == auth method
@ -846,6 +856,8 @@ impl Oauth2ResourceServersReadTransaction {
// Map from spn // Map from spn
preferred_username: Some(code_xchg.uat.spn.clone()), preferred_username: Some(code_xchg.uat.spn.clone()),
scopes: code_xchg.scopes.clone(), scopes: code_xchg.scopes.clone(),
email,
email_verified,
..Default::default() ..Default::default()
}, },
claims: 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()]); let amr = Some(vec![at.auth_type.to_string()]);
// ==== good to generate response ==== // ==== good to generate response ====
@ -1058,6 +1080,8 @@ impl Oauth2ResourceServersReadTransaction {
// Map from spn // Map from spn
preferred_username: Some(account.spn), preferred_username: Some(account.spn),
scopes: at.scopes, scopes: at.scopes,
email,
email_verified,
..Default::default() ..Default::default()
}, },
claims: Default::default(), claims: Default::default(),

View file

@ -1283,18 +1283,8 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// Check the password quality. // Check the password quality.
// Ask if tis all good - this step checks pwpolicy and such // 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() { self.check_password_quality(pce.cleartext.as_str(), account.related_inputs().as_slice())
related_inputs.push(s.as_str())
};
self.check_password_quality(pce.cleartext.as_str(), related_inputs.as_slice())
.map_err(|e| { .map_err(|e| {
request_error!(err = ?e, "check_password_quality"); request_error!(err = ?e, "check_password_quality");
e e
@ -1370,18 +1360,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// If we got here, then pre-apply succedded, and that means access control // If we got here, then pre-apply succedded, and that means access control
// passed. Now we can do the extra checks. // passed. Now we can do the extra checks.
// Get related inputs, such as account name, email, etc. self.check_password_quality(pce.cleartext.as_str(), account.related_inputs().as_slice())
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())
.map_err(|e| { .map_err(|e| {
admin_error!(?e, "Failed to checked password quality"); admin_error!(?e, "Failed to checked password quality");
e e

View file

@ -30,6 +30,7 @@ pub(crate) struct UnixUserAccount {
pub valid_from: Option<OffsetDateTime>, pub valid_from: Option<OffsetDateTime>,
pub expire: Option<OffsetDateTime>, pub expire: Option<OffsetDateTime>,
pub radius_secret: Option<String>, pub radius_secret: Option<String>,
pub mail: Vec<String>,
} }
lazy_static! { lazy_static! {
@ -94,6 +95,11 @@ macro_rules! try_from_entry {
.get_ava_single_secret("radius_secret") .get_ava_single_secret("radius_secret")
.map(str::to_string); .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 valid_from = $value.get_ava_single_datetime("account_valid_from");
let expire = $value.get_ava_single_datetime("account_expire"); let expire = $value.get_ava_single_datetime("account_expire");
@ -111,6 +117,7 @@ macro_rules! try_from_entry {
valid_from, valid_from,
expire, expire,
radius_secret, radius_secret,
mail,
}) })
}}; }};
} }
@ -202,6 +209,21 @@ impl UnixUserAccount {
vmin && vmax 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( pub(crate) fn verify_unix_credential(
&self, &self,
cleartext: &str, cleartext: &str,

View file

@ -2225,6 +2225,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE, JSON_SCHEMA_ATTR_OAUTH2_JWT_LEGACY_CRYPTO_ENABLE,
JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER, JSON_SCHEMA_ATTR_RS256_PRIVATE_KEY_DER,
JSON_SCHEMA_CLASS_PERSON, JSON_SCHEMA_CLASS_PERSON,
JSON_SCHEMA_CLASS_ORGPERSON,
JSON_SCHEMA_CLASS_GROUP, JSON_SCHEMA_CLASS_GROUP,
JSON_SCHEMA_CLASS_ACCOUNT, JSON_SCHEMA_CLASS_ACCOUNT,
JSON_SCHEMA_CLASS_DOMAIN_INFO, JSON_SCHEMA_CLASS_DOMAIN_INFO,
@ -2302,6 +2303,9 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_PEOPLE_EXTEND_PRIV_V1, JSON_IDM_PEOPLE_EXTEND_PRIV_V1,
JSON_IDM_PEOPLE_WRITE_PRIV_V1, JSON_IDM_PEOPLE_WRITE_PRIV_V1,
JSON_IDM_PEOPLE_READ_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_MANAGE_PRIV_V1,
JSON_IDM_GROUP_WRITE_PRIV_V1, JSON_IDM_GROUP_WRITE_PRIV_V1,
JSON_IDM_GROUP_UNIX_EXTEND_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_GROUP_UNIX_EXTEND_PRIV_V1,
JSON_IDM_ACP_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1, JSON_IDM_ACP_PEOPLE_ACCOUNT_PASSWORD_IMPORT_PRIV_V1,
JSON_IDM_ACP_PEOPLE_EXTEND_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_ACCOUNT_UNIX_EXTEND_PRIV_V1,
JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1, JSON_IDM_HP_ACP_GROUP_UNIX_EXTEND_PRIV_V1,
JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1, JSON_IDM_HP_ACP_OAUTH2_MANAGE_PRIV_V1,

View file

@ -17,8 +17,8 @@ use tracing::span::{Attributes, Record};
use tracing::{Event, Id, Level, Metadata, Subscriber}; use tracing::{Event, Id, Level, Metadata, Subscriber};
use tracing_subscriber::layer::{Context, Layered, SubscriberExt}; use tracing_subscriber::layer::{Context, Layered, SubscriberExt};
use tracing_subscriber::registry::{LookupSpan, Registry, Scope, SpanRef}; use tracing_subscriber::registry::{LookupSpan, Registry, Scope, SpanRef};
use tracing_subscriber::Layer;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;
use uuid::Uuid; use uuid::Uuid;
use crate::tracing_tree::processor::TestProcessor; use crate::tracing_tree::processor::TestProcessor;

View file

@ -110,7 +110,9 @@ impl From<Value> for ValueSet {
Value::Nsuniqueid(s) => I::Nsuniqueid(btreeset![s]), Value::Nsuniqueid(s) => I::Nsuniqueid(btreeset![s]),
Value::DateTime(dt) => I::DateTime(smolset![dt]), Value::DateTime(dt) => I::DateTime(smolset![dt]),
Value::EmailAddress(e, _) => I::EmailAddress { 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], set: btreeset![e],
}, },
Value::Url(u) => I::Url(smolset![u]), Value::Url(u) => I::Url(smolset![u]),
@ -179,13 +181,10 @@ impl TryFrom<DbValueV1> for ValueSet {
} }
DbValueV1::EmailAddress(DbValueEmailAddressV1 { DbValueV1::EmailAddress(DbValueEmailAddressV1 {
d: email_addr, d: email_addr,
p: is_primary, p: _,
}) => { }) => {
let primary = if is_primary { // Since this is the first, we need to disregard the primary.
Some(email_addr.clone()) let primary = Some(email_addr.clone());
} else {
None
};
I::EmailAddress { I::EmailAddress {
primary, primary,
set: btreeset![email_addr], set: btreeset![email_addr],
@ -377,7 +376,6 @@ impl ValueSet {
}), }),
) => { ) => {
if is_primary { if is_primary {
debug_assert!(primary.is_none());
*primary = Some(email_addr.clone()); *primary = Some(email_addr.clone());
}; };
@ -391,7 +389,6 @@ impl ValueSet {
}), }),
) => { ) => {
if is_primary { if is_primary {
debug_assert!(primary.is_none());
*primary = Some(phone_number.clone()); *primary = Some(phone_number.clone());
}; };
Ok(set.insert(phone_number)) Ok(set.insert(phone_number))
@ -711,10 +708,12 @@ impl ValueSet {
I::DateTime(set) => { I::DateTime(set) => {
set.clear(); set.clear();
} }
I::EmailAddress { primary: _, set } => { I::EmailAddress { primary, set } => {
*primary = None;
set.clear(); set.clear();
} }
I::PhoneNumber { primary: _, set } => { I::PhoneNumber { primary, set } => {
*primary = None;
set.clear(); set.clear();
} }
I::Address { set } => { I::Address { set } => {
@ -797,10 +796,11 @@ impl ValueSet {
set.remove(dt); set.remove(dt);
} }
(I::EmailAddress { primary, set }, PartialValue::EmailAddress(e)) => { (I::EmailAddress { primary, set }, PartialValue::EmailAddress(e)) => {
if Some(e) == primary.as_ref() {
*primary = None;
};
set.remove(e); set.remove(e);
if Some(e) == primary.as_ref() {
*primary = set.iter().cloned().next();
// *primary = None;
};
} }
(I::PhoneNumber { primary, set }, PartialValue::PhoneNumber(e)) => { (I::PhoneNumber { primary, set }, PartialValue::PhoneNumber(e)) => {
if Some(e) == primary.as_ref() { 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>> { pub fn as_sshkey_map(&self) -> Option<&BTreeMap<String, String>> {
match &self.inner { match &self.inner {
I::SshKey(map) => Some(map), 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>> { pub fn as_oauthscope_iter(&self) -> Option<impl Iterator<Item = &str>> {
match &self.inner { match &self.inner {
I::OauthScope(set) => Some(set.iter().map(|s| s.as_str())), 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")); ValueSet::new(Value::new_email_address_s("claire@example.com").expect("Invalid Email"));
assert!(vs.len() == 1); 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. // Add another, still not primary.
assert!( assert!(
@ -2378,7 +2392,7 @@ mod tests {
); );
assert!(vs.len() == 2); 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 // Update primary
assert!( assert!(
@ -2396,17 +2410,22 @@ mod tests {
assert!(vs == vs2); assert!(vs == vs2);
assert!(vs.to_email_address_primary_str() == vs2.to_email_address_primary_str()); 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.remove(&PartialValue::new_email_address_s("primary@example.com")));
assert!(vs.len() == 2); assert!(vs.len() == 2);
assert!(vs.to_email_address_primary_str().is_none()); assert!(vs.to_email_address_primary_str() == Some("alice@example.com"));
// Restore form dbv1, no primary.
// Restore from dbv1, alice persisted.
let vs3 = ValueSet::from_db_valuev1_iter(vs.to_db_valuev1_iter()) let vs3 = ValueSet::from_db_valuev1_iter(vs.to_db_valuev1_iter())
.expect("Failed to construct vs2 from dbvalue"); .expect("Failed to construct vs2 from dbvalue");
assert!(vs == vs3); assert!(vs == vs3);
assert!(vs.to_email_address_primary_str() != vs2.to_email_address_primary_str()); assert!(vs3.len() == 2);
assert!(vs.to_email_address_primary_str() == vs3.to_email_address_primary_str()); 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());
} }
} }