mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
fb12a1a86b
commit
0c3ce226cf
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -233,6 +233,8 @@ pub struct AccountPersonOpt {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum AccountPerson {
|
||||
#[structopt(name = "extend")]
|
||||
Extend(AccountPersonOpt),
|
||||
#[structopt(name = "set")]
|
||||
Set(AccountPersonOpt),
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")?;
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue