Add 'account person set' command (#667)

* Add 'account person set' command

This command allows a user to modify, say, their legal name in a
self-service fashion.

This wasn't possible before by default since the 'extend' operation
required additional ACPs in order to operate which not every user would
have.

The new "person set" api is compatible with the default self_write ACP,
and so allows self-service modification.

* Add a short section on people attributes to the book
This commit is contained in:
Euan Kemp 2022-04-01 20:24:07 -07:00 committed by GitHub
parent fb12a1a86b
commit 0c3ce226cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 149 additions and 11 deletions

View file

@ -117,6 +117,30 @@ where the "valid from" is *after* the expire_at, the expire_at will be respected
These validity settings impact all authentication functions of the account (kanidm, ldap, radius).
## People Accounts
Kanidm allows extending accounts to include additional "people" attributes,
such as their legal name and email address.
Initially, an account does not have these attributes. If desired, an account
may be modified to have these "person" attributes like so:
# Note, both the --legalname and --mail flags may be omitted
kanidm account person extend demo_user --legalname "initial name" --mail "initial@email.address"
Once an account has been extended, the "person" attributes may be set by the
user of the account, or anyone with enough privileges.
Whether an account is currently a "person" or not can be identified from the "account get" output:
kanidm account get demo_user
# ---
# class: person
# ... (other output omitted)
The presence of a "class: person" stanza indicates that this account may have
"people" attributes.
## Why Can't I Change admin With idm_admin?
As a security mechanism there is a distinction between "accounts" and "high permission

View file

@ -1314,7 +1314,7 @@ impl KanidmAsyncClient {
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
let px = AccountPersonExtend {
let px = AccountPersonSet {
mail: mail.map(|s| s.to_vec()),
legalname: legalname.map(str::to_string),
};
@ -1322,6 +1322,20 @@ impl KanidmAsyncClient {
.await
}
pub async fn idm_account_person_set(
&self,
id: &str,
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
let px = AccountPersonSet {
mail: mail.map(|s| s.to_vec()),
legalname: legalname.map(str::to_string),
};
self.perform_post_request(format!("/v1/account/{}/_person/_set", id).as_str(), px)
.await
}
pub async fn idm_account_get_ssh_pubkey(
&self,
id: &str,

View file

@ -850,6 +850,15 @@ impl KanidmClient {
tokio_block_on(self.asclient.idm_account_person_extend(id, mail, legalname))
}
pub fn idm_account_person_set(
&self,
id: &str,
mail: Option<&[String]>,
legalname: Option<&str>,
) -> Result<(), ClientError> {
tokio_block_on(self.asclient.idm_account_person_set(id, mail, legalname))
}
/*
pub fn idm_account_rename_ssh_pubkey(&self, id: &str, oldtag: &str, newtag: &str) -> Result<(), ClientError> {
self.perform_put_request(format!("/v1/account/{}/_ssh_pubkeys/{}", id, oldtag).as_str(), newtag.to_string())

View file

@ -95,9 +95,18 @@ fn is_attr_writable(rsclient: &KanidmClient, id: &str, attr: &str) -> Option<boo
.idm_account_unix_cred_put(id, "dsadjasiodqwjk12asdl")
.is_ok(),
),
"legalname" => Some(
rsclient
.idm_account_person_set(id, None, Some("test legal name".into()))
.is_ok(),
),
"mail" => Some(
rsclient
.idm_account_person_set(id, Some(&[format!("{}@example.com", id)]), None)
.is_ok(),
),
entry => {
let new_value = match entry {
"mail" => format!("{}@example.com", id),
"acp_receiver" => r#"{"eq":["memberof","00000000-0000-0000-0000-000000000011"]}"#.to_string(),
"acp_targetscope" => "{\"and\": [{\"eq\": [\"class\",\"access_control_profile\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}".to_string(),
_ => id.to_string(),

View file

@ -322,7 +322,7 @@ pub struct AccountUnixExtend {
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AccountPersonExtend {
pub struct AccountPersonSet {
pub mail: Option<Vec<String>>,
pub legalname: Option<String>,
}

View file

@ -39,6 +39,7 @@ impl AccountOpt {
AccountPosix::SetPassword(apo) => apo.copt.debug,
},
AccountOpt::Person(apopt) => match apopt {
AccountPerson::Extend(apo) => apo.copt.debug,
AccountPerson::Set(apo) => apo.copt.debug,
},
AccountOpt::Ssh(asopt) => match asopt {
@ -396,7 +397,7 @@ impl AccountOpt {
}
}, // end AccountOpt::Posix
AccountOpt::Person(apopt) => match apopt {
AccountPerson::Set(aopt) => {
AccountPerson::Extend(aopt) => {
let client = aopt.copt.to_client();
if let Err(e) = client.idm_account_person_extend(
aopt.aopts.account_id.as_str(),
@ -406,6 +407,16 @@ impl AccountOpt {
error!("Error -> {:?}", e);
}
}
AccountPerson::Set(aopt) => {
let client = aopt.copt.to_client();
if let Err(e) = client.idm_account_person_set(
aopt.aopts.account_id.as_str(),
aopt.mail.as_deref(),
aopt.legalname.as_deref(),
) {
error!("Error -> {:?}", e);
}
}
}, // end AccountOpt::Person
AccountOpt::Ssh(asopt) => match asopt {
AccountSsh::List(aopt) => {

View file

@ -233,6 +233,8 @@ pub struct AccountPersonOpt {
#[derive(Debug, StructOpt)]
pub enum AccountPerson {
#[structopt(name = "extend")]
Extend(AccountPersonOpt),
#[structopt(name = "set")]
Set(AccountPersonOpt),
}

View file

@ -524,6 +524,9 @@ pub fn create_https_server(
account_route
.at("/:id/_person/_extend")
.post(account_post_id_person_extend);
account_route
.at("/:id/_person/_set")
.post(account_post_id_person_set);
account_route.at("/:id/_lock").get(do_nothing);
account_route.at("/:id/_credential").get(do_nothing);

View file

@ -6,7 +6,7 @@ use kanidm::status::StatusRequestEvent;
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::{
AccountPersonExtend, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
AccountPersonSet, AccountUnixExtend, AuthRequest, AuthResponse, AuthState as ProtoAuthState,
CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest, OperationError, SearchRequest,
SetCredentialRequest, SingleStringRequest,
};
@ -556,7 +556,7 @@ pub async fn account_get_id_radius_token(req: tide::Request<AppState>) -> tide::
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 obj: AccountPersonSet = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
@ -566,6 +566,19 @@ pub async fn account_post_id_person_extend(mut req: tide::Request<AppState>) ->
to_tide_response(res, hvalue)
}
pub async fn account_post_id_person_set(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let obj: AccountPersonSet = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
.qe_w_ref
.handle_idmaccountpersonset(uat, uuid_or_name, obj, eventid)
.await;
to_tide_response(res, hvalue)
}
pub async fn account_post_id_unix(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;

View file

@ -28,7 +28,7 @@ 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::{
AccountPersonExtend, AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend,
AccountPersonSet, AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend,
ModifyRequest, SetCredentialRequest, SetCredentialResponse,
};
@ -963,13 +963,11 @@ impl QueryServerWriteV1 {
&self,
uat: Option<String>,
uuid_or_name: String,
px: AccountPersonExtend,
px: AccountPersonSet,
eventid: Uuid,
) -> Result<(), OperationError> {
let AccountPersonExtend { mail, legalname } = px;
let AccountPersonSet { mail, legalname } = px;
// The filter_map here means we only create the mods if the gidnumber or shell are set
// in the actual request.
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")));
@ -1011,6 +1009,61 @@ impl QueryServerWriteV1 {
res
}
#[instrument(
level = "trace",
name = "idmaccountpersonset",
skip(self, uat, uuid_or_name, eventid)
fields(uuid = ?eventid)
)]
pub async fn handle_idmaccountpersonset(
&self,
uat: Option<String>,
uuid_or_name: String,
px: AccountPersonSet,
eventid: Uuid,
) -> Result<(), OperationError> {
let AccountPersonSet { mail, legalname } = px;
let mut mods: Vec<_> = Vec::with_capacity(3 + mail.as_ref().map(|v| v.len()).unwrap_or(0));
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);
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
let res = self
.modify_from_internal_parts(uat, &uuid_or_name, &ml, filter)
.await;
res
}
#[instrument(
level = "trace",
name = "idmaccountunixextend",