6 create modify tool (#113)

Implements #6 - create, modify and delete. These are the raw/lowlevel db commands which are really useful for administrators. They aren't intended for normal day to day use though.

This also adds a basic getting started, fixes a missing privilege, adds support for reseting another accounts password, and for server side password generation.

It's likely I'm going to reformat some of the current REST api though to use our higher level internal types.
This commit is contained in:
Firstyear 2019-10-07 08:41:30 +10:00 committed by GitHub
parent 1f2b965285
commit 6c44297bd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 742 additions and 55 deletions

44
GETTING_STARTED.md Normal file
View file

@ -0,0 +1,44 @@
# Getting Started
WARNING: This document is still in progress, and due to the high rate of change in the cli
tooling, may be OUT OF DATE or otherwise incorrect. If you have questions, please get
in contact!
Create the service account
cargo run -- raw create -H https://localhost:8080 -C ../insecure/ca.pem -D admin example.create.account.json
Give it permissions
cargo run -- raw modify -H https://localhost:8080 -C ../insecure/ca.pem -D admin '{"Or": [ {"Eq": ["name", "idm_person_account_create_priv"]}, {"Eq": ["name", "idm_service_account_create_priv"]}, {"Eq": ["name", "idm_account_write_priv"]}, {"Eq": ["name", "idm_group_write_priv"]}, {"Eq": ["name", "idm_people_write_priv"]}, {"Eq": ["name", "idm_group_create_priv"]} ]}' example.modify.idm_admin.json
Show the account details now:
cargo run -- raw search -H https://localhost:8080 -C ../insecure/ca.pem -D admin '{"Eq": ["name", "idm_admin"]}'
> Entry { attrs: {"class": ["account", "memberof", "object"], "displayname": ["IDM Admin"], "memberof": ["idm_people_read_priv", "idm_people_write_priv", "idm_group_write_priv", "idm_account_read_priv", "idm_account_write_priv", "idm_service_account_create_priv", "idm_person_account_create_priv", "idm_high_privilege"], "name": ["idm_admin"], "uuid": ["bb852c38-8920-4932-a551-678253cae6ff"]} }
Set the password
cargo run -- account credential set_password -H https://localhost:8080 -C ../insecure/ca.pem -D admin idm_admin
Or even better:
cargo run -- account credential generate_password -H https://localhost:8080 -C ../insecure/ca.pem -D admin idm_admin
Show it works:
cargo run -- self whoami -H 'https://localhost:8080' -C ../insecure/ca.pem -D idm_admin
Now our service account can create and administer accounts and groups:
cargo run -- raw create -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin example.create.group.json
And of course, as the idm_admin, we can't write back to admin:
cargo run -- account credential generate_password -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin admin
Nor can we escalate privs (we are not allow to modify HP groups):
cargo run -- raw modify -H https://localhost:8080 -C ../insecure/ca.pem -D idm_admin '{"Eq": ["name", "idm_admins"]}' example.modify.idm_admin.json
So we have a secure way to manage the identities in the directory, without giving full control to any one account!

View file

@ -65,6 +65,10 @@ In a new terminal, you can now build and run the client tools with:
cargo run -- whoami -H https://localhost:8080 -D anonymous -C ../insecure/ca.pem
cargo run -- whoami -H https://localhost:8080 -D admin -C ../insecure/ca.pem
For more see [getting started]
[getting started]: https://github.com/Firstyear/kanidm/blob/master/GETTING_STARTED.html
## Development and Testing
There are tests of various components through the various components of the project. When developing

View file

@ -11,9 +11,9 @@ use std::fs::File;
use std::io::Read;
use kanidm_proto::v1::{
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, Filter,
ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse,
SingleStringRequest, UserAuthToken, WhoamiResponse,
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
Entry, Filter, ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse,
SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse,
};
use serde_json;
@ -24,6 +24,7 @@ pub enum ClientError {
Transport(reqwest::Error),
AuthenticationFailed,
JsonParse,
EmptyResponse,
}
#[derive(Debug)]
@ -53,6 +54,17 @@ impl KanidmClient {
}
}
pub fn new_session(&self) -> Self {
let new_client =
Self::build_reqwest(&self.ca).expect("Unexpected reqwest builder failure!");
KanidmClient {
client: new_client,
addr: self.addr.clone(),
ca: self.ca.clone(),
}
}
fn build_reqwest(ca: &Option<reqwest::Certificate>) -> Result<reqwest::Client, reqwest::Error> {
let client_builder = reqwest::Client::builder().cookie_store(true);
@ -97,6 +109,33 @@ impl KanidmClient {
Ok(r)
}
fn perform_put_request<R: Serialize, T: DeserializeOwned>(
&self,
dest: &str,
request: R,
) -> Result<T, ClientError> {
let dest = format!("{}{}", self.addr, dest);
let req_string = serde_json::to_string(&request).unwrap();
let mut response = self
.client
.put(dest.as_str())
.body(req_string)
.send()
.map_err(|e| ClientError::Transport(e))?;
match response.status() {
reqwest::StatusCode::OK => {}
unexpect => return Err(ClientError::Http(unexpect)),
}
// TODO: What about errors
let r: T = response.json().unwrap();
Ok(r)
}
fn perform_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
let dest = format!("{}{}", self.addr, dest);
let mut response = self
@ -185,14 +224,6 @@ impl KanidmClient {
}
// search
pub fn search_str(&self, query: &str) -> Result<Vec<Entry>, ClientError> {
let filter: Filter = serde_json::from_str(query).map_err(|e| {
error!("JSON Parse Failure -> {:?}", e);
ClientError::JsonParse
})?;
self.search(filter)
}
pub fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
let sr = SearchRequest { filter: filter };
let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr);
@ -216,6 +247,13 @@ impl KanidmClient {
r.map(|_| ())
}
// delete
pub fn delete(&self, filter: Filter) -> Result<(), ClientError> {
let dr = DeleteRequest { filter: filter };
let r: Result<OperationResponse, _> = self.perform_post_request("/v1/raw/delete", dr);
r.map(|_| ())
}
// === idm actions here ==
pub fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> {
let s = SingleStringRequest { value: cleartext };
@ -256,6 +294,36 @@ impl KanidmClient {
self.perform_get_request(format!("/v1/account/{}", id).as_str())
}
// different ways to set the primary credential?
// not sure how to best expose this.
pub fn idm_account_primary_credential_set_password(
&self,
id: &str,
pw: &str,
) -> Result<(), ClientError> {
let r = SetAuthCredential::Password(pw.to_string());
let res: Result<Option<String>, _> = self.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
);
res.map(|_| ())
}
pub fn idm_account_primary_credential_set_generated(
&self,
id: &str,
) -> Result<String, ClientError> {
let r = SetAuthCredential::GeneratePassword;
self.perform_put_request(
format!("/v1/account/{}/_credential/primary", id).as_str(),
r,
)
.and_then(|v| match v {
Some(p) => Ok(p),
None => Err(ClientError::EmptyResponse),
})
}
// ==== schema
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/schema")

View file

@ -186,7 +186,7 @@ fn test_server_search() {
assert!(res.is_ok());
let rset = rsclient
.search_str("{\"Eq\":[\"name\", \"admin\"]}")
.search(Filter::Eq("name".to_string(), "admin".to_string()))
.unwrap();
println!("{:?}", rset);
let e = rset.first().unwrap();
@ -224,6 +224,57 @@ fn test_server_admin_change_simple_password() {
});
}
// Add a test for reseting another accounts pws via the rest api
#[test]
fn test_server_admin_reset_simple_password() {
run_test(|rsclient: KanidmClient| {
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
assert!(res.is_ok());
// Create a diff account
let e: Entry = serde_json::from_str(
r#"{
"attrs": {
"class": ["person", "account"],
"name": ["testperson"],
"displayname": ["testperson"]
}
}"#,
)
.unwrap();
// Not logged in - should fail!
let res = rsclient.create(vec![e.clone()]);
assert!(res.is_ok());
// By default, admin's can't actually administer accounts, so mod them into
// the account admin group.
let f = Filter::Eq("name".to_string(), "idm_account_write_priv".to_string());
let m = ModifyList::new_list(vec![Modify::Present(
"member".to_string(),
"idm_admins".to_string(),
)]);
let res = rsclient.modify(f.clone(), m.clone());
assert!(res.is_ok());
// Now set it's password.
let res = rsclient.idm_account_primary_credential_set_password("testperson", "password");
assert!(res.is_ok());
// Check it stuck.
let tclient = rsclient.new_session();
assert!(tclient
.auth_simple_password("testperson", "password")
.is_ok());
// Generate a pw instead
let res = rsclient.idm_account_primary_credential_set_generated("testperson");
assert!(res.is_ok());
let gpw = res.unwrap();
let tclient = rsclient.new_session();
assert!(tclient
.auth_simple_password("testperson", gpw.as_str())
.is_ok());
});
}
// test the rest group endpoint.
#[test]
fn test_server_rest_group_read() {

View file

@ -308,6 +308,15 @@ pub struct AuthResponse {
pub state: AuthState,
}
// Types needed for setting credentials
#[derive(Debug, Serialize, Deserialize)]
pub enum SetAuthCredential {
Password(String),
GeneratePassword,
// TOTP()
// Webauthn(response)
}
/* Recycle Requests area */
// Only two actions on recycled is possible. Search and Revive.

View file

@ -10,8 +10,11 @@ path = "src/main.rs"
[dependencies]
kanidm_client = { path = "../kanidm_client" }
kanidm_proto = { path = "../kanidm_proto" }
rpassword = "0.4"
structopt = { version = "0.2", default-features = false }
log = "0.4"
env_logger = "0.6"
serde = "1.0"
serde_json = "1.0"

View file

@ -0,0 +1,10 @@
[
{
"name": ["idm_admin"],
"class": ["account"],
"displayname": ["IDM Admin"],
"description": ["Default IDM Admin"]
}
]

View file

@ -0,0 +1,6 @@
[
{
"name": ["demo_group"],
"class": ["group"]
}
]

View file

@ -0,0 +1,5 @@
[
{"Present": ["member", "idm_admin"]}
]

View file

@ -0,0 +1,4 @@
[
{ "Purged": "name" },
{ "Present": ["name", "demo_group"] }
]

View file

@ -1,7 +1,16 @@
extern crate structopt;
use kanidm_client::KanidmClient;
use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList};
use serde::de::DeserializeOwned;
use std::path::PathBuf;
use structopt::StructOpt;
use std::collections::BTreeMap;
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
extern crate env_logger;
#[macro_use]
extern crate log;
@ -40,25 +49,87 @@ impl CommonOpt {
}
#[derive(Debug, StructOpt)]
struct SearchOpt {
struct FilterOpt {
#[structopt()]
filter: String,
#[structopt(flatten)]
commonopts: CommonOpt,
}
#[derive(Debug, StructOpt)]
struct CreateOpt {
#[structopt(parse(from_os_str))]
file: Option<PathBuf>,
#[structopt(flatten)]
commonopts: CommonOpt,
}
#[derive(Debug, StructOpt)]
struct ModifyOpt {
#[structopt(flatten)]
commonopts: CommonOpt,
#[structopt()]
filter: String,
#[structopt(parse(from_os_str))]
file: Option<PathBuf>,
}
#[derive(Debug, StructOpt)]
enum RawOpt {
#[structopt(name = "search")]
Search(FilterOpt),
#[structopt(name = "create")]
Create(CreateOpt),
#[structopt(name = "modify")]
Modify(ModifyOpt),
#[structopt(name = "delete")]
Delete(FilterOpt),
}
#[derive(Debug, StructOpt)]
struct AccountCommonOpt {
#[structopt()]
account_id: String,
}
#[derive(Debug, StructOpt)]
struct AccountCredentialSet {
#[structopt(flatten)]
aopts: AccountCommonOpt,
#[structopt()]
application_id: Option<String>,
#[structopt(flatten)]
copt: CommonOpt,
}
#[derive(Debug, StructOpt)]
enum AccountCredential {
#[structopt(name = "set_password")]
SetPassword(AccountCredentialSet),
#[structopt(name = "generate_password")]
GeneratePassword(AccountCredentialSet),
}
#[derive(Debug, StructOpt)]
enum AccountOpt {
#[structopt(name = "credential")]
Credential(AccountCredential),
}
#[derive(Debug, StructOpt)]
enum SelfOpt {
#[structopt(name = "whoami")]
Whoami(CommonOpt),
#[structopt(name = "set_password")]
SetPassword(CommonOpt),
}
#[derive(Debug, StructOpt)]
enum ClientOpt {
#[structopt(name = "search")]
Search(SearchOpt),
#[structopt(name = "whoami")]
Whoami(CommonOpt),
#[structopt(name = "raw")]
Raw(RawOpt),
#[structopt(name = "self")]
CSelf(SelfOpt),
#[structopt(name = "account")]
Account(AccountOpt),
}
@ -66,15 +137,31 @@ enum ClientOpt {
impl ClientOpt {
fn debug(&self) -> bool {
match self {
ClientOpt::Whoami(copt) => copt.debug,
ClientOpt::Search(sopt) => sopt.commonopts.debug,
ClientOpt::Raw(ropt) => match ropt {
RawOpt::Search(sopt) => sopt.commonopts.debug,
RawOpt::Create(copt) => copt.commonopts.debug,
RawOpt::Modify(mopt) => mopt.commonopts.debug,
RawOpt::Delete(dopt) => dopt.commonopts.debug,
},
ClientOpt::CSelf(csopt) => match csopt {
SelfOpt::Whoami(copt) => copt.debug,
SelfOpt::SetPassword(copt) => copt.debug,
},
ClientOpt::Account(aopt) => match aopt {
AccountOpt::SetPassword(copt) => copt.debug,
_ => false,
},
}
}
}
fn read_file<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T, Box<dyn Error>> {
let f = File::open(path)?;
let r = BufReader::new(f);
let t: T = serde_json::from_reader(r)?;
Ok(t)
}
fn main() {
let opt = ClientOpt::from_args();
@ -86,7 +173,54 @@ fn main() {
env_logger::init();
match opt {
ClientOpt::Whoami(copt) => {
ClientOpt::Raw(ropt) => match ropt {
RawOpt::Search(sopt) => {
let client = sopt.commonopts.to_client();
let filter: Filter = serde_json::from_str(sopt.filter.as_str()).unwrap();
let rset = client.search(filter).unwrap();
rset.iter().for_each(|e| {
println!("{:?}", e);
});
}
RawOpt::Create(copt) => {
let client = copt.commonopts.to_client();
// Read the file?
match copt.file {
Some(p) => {
let r_entries: Vec<BTreeMap<String, Vec<String>>> = read_file(p).unwrap();
let entries = r_entries.into_iter().map(|b| Entry { attrs: b }).collect();
client.create(entries).unwrap()
}
None => {
println!("Must provide a file");
}
}
}
RawOpt::Modify(mopt) => {
let client = mopt.commonopts.to_client();
// Read the file?
match mopt.file {
Some(p) => {
let filter: Filter = serde_json::from_str(mopt.filter.as_str()).unwrap();
let r_list: Vec<Modify> = read_file(p).unwrap();
let modlist = ModifyList::new_list(r_list);
client.modify(filter, modlist).unwrap()
}
None => {
println!("Must provide a file");
}
}
}
RawOpt::Delete(dopt) => {
let client = dopt.commonopts.to_client();
let filter: Filter = serde_json::from_str(dopt.filter.as_str()).unwrap();
client.delete(filter).unwrap();
}
},
ClientOpt::CSelf(csopt) => match csopt {
SelfOpt::Whoami(copt) => {
let client = copt.to_client();
match client.whoami() {
@ -100,17 +234,8 @@ fn main() {
Err(e) => println!("Error: {:?}", e),
}
}
ClientOpt::Search(sopt) => {
let client = sopt.commonopts.to_client();
let rset = client.search_str(sopt.filter.as_str()).unwrap();
for e in rset {
println!("{:?}", e);
}
}
ClientOpt::Account(aopt) => match aopt {
AccountOpt::SetPassword(copt) => {
SelfOpt::SetPassword(copt) => {
let client = copt.to_client();
let password = rpassword::prompt_password_stderr("Enter new password: ").unwrap();
@ -118,5 +243,37 @@ fn main() {
client.idm_account_set_password(password).unwrap();
}
},
ClientOpt::Account(aopt) => match aopt {
// id/cred/primary/set
AccountOpt::Credential(acopt) => match acopt {
AccountCredential::SetPassword(acsopt) => {
let client = acsopt.copt.to_client();
let password = rpassword::prompt_password_stderr(
format!("Enter new password for {}: ", acsopt.aopts.account_id).as_str(),
)
.unwrap();
client
.idm_account_primary_credential_set_password(
acsopt.aopts.account_id.as_str(),
password.as_str(),
)
.unwrap();
}
AccountCredential::GeneratePassword(acsopt) => {
let client = acsopt.copt.to_client();
let npw = client
.idm_account_primary_credential_set_generated(
acsopt.aopts.account_id.as_str(),
)
.unwrap();
println!(
"Generated password for {}: {}",
acsopt.aopts.account_id, npw
);
}
}, // end AccountOpt::Credential
},
}
}

View file

@ -555,7 +555,9 @@ pub trait AccessControlsTransaction {
})
.collect();
audit_log!(audit, "Related acs -> {:?}", related_acp);
related_acp.iter().for_each(|racp| {
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
});
// Get the set of attributes requested by the caller
// TODO #69: This currently
@ -684,7 +686,9 @@ pub trait AccessControlsTransaction {
})
.collect();
audit_log!(audit, "Related acs -> {:?}", related_acp);
related_acp.iter().for_each(|racp| {
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
});
// build two sets of "requested pres" and "requested rem"
let requested_pres: BTreeSet<&str> = me
@ -1020,7 +1024,9 @@ pub trait AccessControlsTransaction {
})
.collect();
audit_log!(audit, "Related acs -> {:?}", related_acp);
related_acp.iter().for_each(|racp| {
audit_log!(audit, "Related acs -> {:?}", racp.acp.name);
});
// For each entry
let r = entries.iter().fold(true, |acc, e| {

View file

@ -7,7 +7,7 @@ use crate::event::{
AuthEvent, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
SearchEvent, SearchResult, WhoamiResult,
};
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
use kanidm_proto::v1::OperationError;
use crate::filter::{Filter, FilterInvalid};
@ -17,7 +17,8 @@ use crate::server::{QueryServer, QueryServerTransaction};
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::{
AuthRequest, AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse,
SearchRequest, SearchResponse, SingleStringRequest, UserAuthToken, WhoamiResponse,
SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
WhoamiResponse,
};
use actix::prelude::*;
@ -160,6 +161,33 @@ impl Message for IdmAccountSetPasswordMessage {
type Result = Result<OperationResponse, OperationError>;
}
pub struct InternalCredentialSetMessage {
pub uat: Option<UserAuthToken>,
pub uuid_or_name: String,
pub appid: Option<String>,
pub sac: SetAuthCredential,
}
impl InternalCredentialSetMessage {
pub fn new(
uat: Option<UserAuthToken>,
uuid_or_name: String,
appid: Option<String>,
sac: SetAuthCredential,
) -> Self {
InternalCredentialSetMessage {
uat: uat,
uuid_or_name: uuid_or_name,
appid: appid,
sac: sac,
}
}
}
impl Message for InternalCredentialSetMessage {
type Result = Result<Option<String>, OperationError>;
}
// ===========================================================
pub struct QueryServerV1 {
@ -480,6 +508,81 @@ impl Handler<InternalSearchMessage> for QueryServerV1 {
}
}
impl Handler<InternalCredentialSetMessage> for QueryServerV1 {
type Result = Result<Option<String>, OperationError>;
fn handle(&mut self, msg: InternalCredentialSetMessage, _: &mut Self::Context) -> Self::Result {
let mut audit = AuditScope::new("internal_credential_set_message");
let res = audit_segment!(&mut audit, || {
let mut idms_prox_write = self.idms.proxy_write();
// given the uuid_or_name, determine the target uuid.
// We can either do this by trying to parse the name or by creating a filter
// to find the entry - there are risks to both TBH ... especially when the uuid
// is also an entries name, but that they aren't the same entry.
let target_uuid = match Uuid::parse_str(msg.uuid_or_name.as_str()) {
Ok(u) => u,
Err(_) => idms_prox_write
.qs_write
.name_to_uuid(&mut audit, msg.uuid_or_name.as_str())
.map_err(|e| {
audit_log!(&mut audit, "Error resolving id to target");
e
})?,
};
// What type of auth set did we recieve?
match msg.sac {
SetAuthCredential::Password(cleartext) => {
let pce = PasswordChangeEvent::from_parts(
&mut audit,
&idms_prox_write.qs_write,
msg.uat,
target_uuid,
cleartext,
msg.appid,
)
.map_err(|e| {
audit_log!(
audit,
"Failed to begin internal_credential_set_message: {:?}",
e
);
e
})?;
idms_prox_write
.set_account_password(&mut audit, &pce)
.and_then(|_| idms_prox_write.commit(&mut audit))
.map(|_| None)
}
SetAuthCredential::GeneratePassword => {
let gpe = GeneratePasswordEvent::from_parts(
&mut audit,
&idms_prox_write.qs_write,
msg.uat,
target_uuid,
msg.appid,
)
.map_err(|e| {
audit_log!(
audit,
"Failed to begin internal_credential_set_message: {:?}",
e
);
e
})?;
idms_prox_write
.generate_account_password(&mut audit, &gpe)
.and_then(|r| idms_prox_write.commit(&mut audit).map(|_| r))
.map(|v| Some(v))
}
}
});
self.log.do_send(audit);
res
}
}
// These below are internal only types.
impl Handler<PurgeTombstoneEvent> for QueryServerV1 {

View file

@ -201,6 +201,18 @@ pub static JSON_IDM_PERSON_ACCOUNT_CREATE_PRIV_V1: &'static str = r#"{
}
}"#;
// IDM_GROUP_CREATE_PRIV
pub static _UUID_IDM_GROUP_CREATE_PRIV: &'static str = "00000000-0000-0000-0000-000000000015";
pub static JSON_IDM_GROUP_CREATE_PRIV_V1: &'static str = r#"{
"attrs": {
"class": ["group", "object"],
"name": ["idm_group_create_priv"],
"uuid": ["00000000-0000-0000-0000-000000000015"],
"description": ["Builtin IDM Group for granting elevated group creation permissions."],
"member": ["00000000-0000-0000-0000-000000000001"]
}
}"#;
// This must be the last group to init to include the UUID of the other high priv groups.
pub static _UUID_IDM_HIGH_PRIVILEGE: &'static str = "00000000-0000-0000-0000-000000001000";
pub static JSON_IDM_HIGH_PRIVILEGE_V1: &'static str = r#"{
@ -590,6 +602,7 @@ pub static JSON_IDM_ACP_SERVICE_ACCOUNT_CREATE_V1: &'static str = r#"{
"class",
"name",
"displayname",
"description",
"primary_credential",
"ssh_publickey"
],
@ -967,6 +980,37 @@ pub static JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1: &'static str = r#"{
// 21 - anonymous / everyone schema read.
// 22 - group create right
pub static _UUID_IDM_ACP_GROUP_CREATE_V1: &'static str = "00000000-0000-0000-0000-ffffff000022";
pub static JSON_IDM_ACP_GROUP_CREATE_V1: &'static str = r#"{
"attrs": {
"class": [
"object",
"access_control_profile",
"access_control_create"
],
"name": ["idm_acp_group_create"],
"uuid": ["00000000-0000-0000-0000-ffffff000022"],
"description": ["Builtin IDM Control for creating groups in the directory"],
"acp_enable": ["true"],
"acp_receiver": [
"{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000015\"]}"
],
"acp_targetscope": [
"{\"And\": [{\"Eq\": [\"class\",\"group\"]}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}"
],
"acp_create_attr": [
"class",
"name",
"description",
"member"
],
"acp_create_class": [
"object", "group"
]
}
}"#;
// Anonymous should be the last opbject in the range here.
pub static JSON_ANONYMOUS_V1: &'static str = r#"{
"attrs": {

View file

@ -15,8 +15,9 @@ use crate::config::Configuration;
// SearchResult
use crate::actors::v1::QueryServerV1;
use crate::actors::v1::{
AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalSearchMessage,
ModifyMessage, SearchMessage, WhoamiMessage,
AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage,
InternalCredentialSetMessage, InternalSearchMessage, ModifyMessage, SearchMessage,
WhoamiMessage,
};
use crate::async_log;
use crate::audit::AuditScope;
@ -34,7 +35,7 @@ use crate::value::PartialValue;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::{
AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest,
SingleStringRequest, UserAuthToken,
SetAuthCredential, SingleStringRequest, UserAuthToken,
};
use uuid::Uuid;
@ -108,9 +109,9 @@ macro_rules! json_event_post {
"Json Decode Failed: {:?}",
e
)))),
}
},
)
} // end match
}, // end closure
) // end and_then
}};
}
@ -218,6 +219,69 @@ fn json_rest_event_get_id(
Box::new(res)
}
fn json_rest_event_credential_put(
id: String,
cred_id: Option<String>,
req: HttpRequest<AppState>,
state: State<AppState>,
) -> impl Future<Item = HttpResponse, Error = Error> {
// what do we need here?
// * a filter of the id to match + class
// * the id of the credential
// * The SetAuthCredential
// * turn into a modlist
// Copy the max size since we move it.
let max_size = state.max_size;
let uat = get_current_user(&req);
req.payload()
.from_err()
.fold(BytesMut::new(), move |mut body, chunk| {
// limit max size of in-memory payload
if (body.len() + chunk.len()) > max_size {
Err(error::ErrorBadRequest("overflow"))
} else {
body.extend_from_slice(&chunk);
Ok(body)
}
})
// `Future::and_then` can be used to merge an asynchronous workflow with a
// synchronous workflow
.and_then(
move |body| -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
let r_obj = serde_json::from_slice::<SetAuthCredential>(&body);
match r_obj {
Ok(obj) => {
let m_obj = InternalCredentialSetMessage::new(uat, id, cred_id, obj);
let res = state.qe.send(m_obj).from_err().and_then(|res| match res {
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
Err(e) => Ok(HttpResponse::InternalServerError().json(e)),
});
Box::new(res)
}
Err(e) => Box::new(future::err(error::ErrorBadRequest(format!(
"Json Decode Failed: {:?}",
e
)))),
} // end match
},
) // end and_then
}
// Okay, so a put normally needs
// * filter of what we are working on (id + class)
// * a BTreeMap<String, Vec<String>> that we turn into a modlist.
//
// OR
// * filter of what we are working on (id + class)
// * a Vec<String> that we are changing
// * the attr name (as a param to this in path)
//
// json_rest_event_put_id(path, req, state
fn schema_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
@ -314,6 +378,13 @@ fn account_get_id(
json_rest_event_get_id(path, req, state, filter)
}
fn account_put_id_credential_primary(
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
let id = path.into_inner();
json_rest_event_credential_put(id, None, req, state)
}
fn group_get(
(req, state): (HttpRequest<AppState>, State<AppState>),
) -> impl Future<Item = HttpResponse, Error = Error> {
@ -928,6 +999,12 @@ pub fn create_server_core(config: Configuration) {
r.method(http::Method::GET).with(do_nothing)
// add delete
})
.resource("/v1/account/{id}/_credential/primary", |r| {
// Set a new primary credential value.
// in future this will tie in to claims.
r.method(http::Method::PUT)
.with_async(account_put_id_credential_primary)
})
.resource("/v1/account/{id}/_credential/{cid}/_lock", |r| {
r.method(http::Method::GET).with(do_nothing)
// add post, delete

View file

@ -5,7 +5,7 @@ use crate::server::QueryServerWriteTransaction;
use uuid::Uuid;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::{OperationError, UserAuthToken};
#[derive(Debug)]
pub struct PasswordChangeEvent {
@ -40,4 +40,47 @@ impl PasswordChangeEvent {
appid: None,
})
}
pub fn from_parts(
audit: &mut AuditScope,
qs: &QueryServerWriteTransaction,
uat: Option<UserAuthToken>,
target: Uuid,
cleartext: String,
appid: Option<String>,
) -> Result<Self, OperationError> {
let e = Event::from_rw_uat(audit, qs, uat)?;
Ok(PasswordChangeEvent {
event: e,
target: target,
cleartext: cleartext,
appid: appid,
})
}
}
#[derive(Debug)]
pub struct GeneratePasswordEvent {
pub event: Event,
pub target: Uuid,
pub appid: Option<String>,
}
impl GeneratePasswordEvent {
pub fn from_parts(
audit: &mut AuditScope,
qs: &QueryServerWriteTransaction,
uat: Option<UserAuthToken>,
target: Uuid,
appid: Option<String>,
) -> Result<Self, OperationError> {
let e = Event::from_rw_uat(audit, qs, uat)?;
Ok(GeneratePasswordEvent {
event: e,
target: target,
appid: appid,
})
}
}

View file

@ -3,9 +3,9 @@ use crate::constants::AUTH_SESSION_TIMEOUT;
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::idm::event::PasswordChangeEvent;
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction};
use crate::utils::{uuid_from_duration, SID};
use crate::utils::{password_from_random, uuid_from_duration, SID};
use crate::value::PartialValue;
use kanidm_proto::v1::AuthState;
@ -282,6 +282,49 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
self.set_account_password(au, &pce)
}
pub fn generate_account_password(
&mut self,
au: &mut AuditScope,
gpe: &GeneratePasswordEvent,
) -> Result<String, OperationError> {
// Get the account
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &gpe.target));
let account = try_audit!(au, Account::try_from_entry(account_entry));
// Ask if tis all good - this step checks pwpolicy and such
// Deny the change if the target account is anonymous!
if account.is_anonymous() {
return Err(OperationError::SystemProtectedObject);
}
// Generate a new random, long pw.
// Because this is generated, we can bypass policy checks!
let cleartext = password_from_random();
// check a password badlist - even if generated, we still don't want to
// reuse something that has been disclosed.
// it returns a modify
let modlist = try_audit!(au, account.gen_password_mod(cleartext.as_str(), &gpe.appid));
audit_log!(au, "processing change {:?}", modlist);
// given the new credential generate a modify
// We use impersonate here to get the event from ae
try_audit!(
au,
self.qs_write.impersonate_modify(
au,
// Filter as executed
filter!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))),
// Filter as intended (acp)
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&gpe.target))),
modlist,
&gpe.event,
)
);
Ok(cleartext)
}
pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> {
self.qs_write.commit(au)
}

View file

@ -1583,6 +1583,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_PEOPLE_WRITE_PRIV_V1,
JSON_IDM_PEOPLE_READ_PRIV_V1,
JSON_IDM_GROUP_WRITE_PRIV_V1,
JSON_IDM_GROUP_CREATE_PRIV_V1,
JSON_IDM_ACCOUNT_WRITE_PRIV_V1,
JSON_IDM_ACCOUNT_READ_PRIV_V1,
JSON_IDM_RADIUS_SERVERS_V1,
@ -1616,6 +1617,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
JSON_IDM_ACP_SCHEMA_WRITE_ATTRS_PRIV_V1,
JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1,
JSON_IDM_ACP_ACP_MANAGER_PRIV_V1,
JSON_IDM_ACP_GROUP_CREATE_V1,
];
let res: Result<(), _> = idm_entries

View file

@ -1,6 +1,9 @@
use std::time::Duration;
use uuid::{Builder, Uuid};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
pub type SID = [u8; 4];
fn uuid_from_u64_u32(a: u64, b: u32, sid: &SID) -> Uuid {
@ -17,6 +20,11 @@ pub fn uuid_from_duration(d: Duration, sid: &SID) -> Uuid {
uuid_from_u64_u32(d.as_secs(), d.subsec_nanos(), sid)
}
pub fn password_from_random() -> String {
let rand_string: String = thread_rng().sample_iter(&Alphanumeric).take(48).collect();
rand_string
}
#[cfg(test)]
mod tests {
use crate::utils::uuid_from_duration;