mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
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:
parent
1f2b965285
commit
6c44297bd9
44
GETTING_STARTED.md
Normal file
44
GETTING_STARTED.md
Normal 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!
|
|
@ -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 anonymous -C ../insecure/ca.pem
|
||||||
cargo run -- whoami -H https://localhost:8080 -D admin -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
|
## Development and Testing
|
||||||
|
|
||||||
There are tests of various components through the various components of the project. When developing
|
There are tests of various components through the various components of the project. When developing
|
||||||
|
|
|
@ -11,9 +11,9 @@ use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, Entry, Filter,
|
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
|
||||||
ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse,
|
Entry, Filter, ModifyList, ModifyRequest, OperationResponse, SearchRequest, SearchResponse,
|
||||||
SingleStringRequest, UserAuthToken, WhoamiResponse,
|
SetAuthCredential, SingleStringRequest, UserAuthToken, WhoamiResponse,
|
||||||
};
|
};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ pub enum ClientError {
|
||||||
Transport(reqwest::Error),
|
Transport(reqwest::Error),
|
||||||
AuthenticationFailed,
|
AuthenticationFailed,
|
||||||
JsonParse,
|
JsonParse,
|
||||||
|
EmptyResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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> {
|
fn build_reqwest(ca: &Option<reqwest::Certificate>) -> Result<reqwest::Client, reqwest::Error> {
|
||||||
let client_builder = reqwest::Client::builder().cookie_store(true);
|
let client_builder = reqwest::Client::builder().cookie_store(true);
|
||||||
|
|
||||||
|
@ -97,6 +109,33 @@ impl KanidmClient {
|
||||||
Ok(r)
|
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> {
|
fn perform_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
|
||||||
let dest = format!("{}{}", self.addr, dest);
|
let dest = format!("{}{}", self.addr, dest);
|
||||||
let mut response = self
|
let mut response = self
|
||||||
|
@ -185,14 +224,6 @@ impl KanidmClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
// search
|
// 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> {
|
pub fn search(&self, filter: Filter) -> Result<Vec<Entry>, ClientError> {
|
||||||
let sr = SearchRequest { filter: filter };
|
let sr = SearchRequest { filter: filter };
|
||||||
let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr);
|
let r: Result<SearchResponse, _> = self.perform_post_request("/v1/raw/search", sr);
|
||||||
|
@ -216,6 +247,13 @@ impl KanidmClient {
|
||||||
r.map(|_| ())
|
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 ==
|
// === idm actions here ==
|
||||||
pub fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> {
|
pub fn idm_account_set_password(&self, cleartext: String) -> Result<(), ClientError> {
|
||||||
let s = SingleStringRequest { value: cleartext };
|
let s = SingleStringRequest { value: cleartext };
|
||||||
|
@ -256,6 +294,36 @@ impl KanidmClient {
|
||||||
self.perform_get_request(format!("/v1/account/{}", id).as_str())
|
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
|
// ==== schema
|
||||||
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
|
pub fn idm_schema_list(&self) -> Result<Vec<Entry>, ClientError> {
|
||||||
self.perform_get_request("/v1/schema")
|
self.perform_get_request("/v1/schema")
|
||||||
|
|
|
@ -186,7 +186,7 @@ fn test_server_search() {
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
|
|
||||||
let rset = rsclient
|
let rset = rsclient
|
||||||
.search_str("{\"Eq\":[\"name\", \"admin\"]}")
|
.search(Filter::Eq("name".to_string(), "admin".to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
println!("{:?}", rset);
|
println!("{:?}", rset);
|
||||||
let e = rset.first().unwrap();
|
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 the rest group endpoint.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_server_rest_group_read() {
|
fn test_server_rest_group_read() {
|
||||||
|
|
|
@ -308,6 +308,15 @@ pub struct AuthResponse {
|
||||||
pub state: AuthState,
|
pub state: AuthState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Types needed for setting credentials
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum SetAuthCredential {
|
||||||
|
Password(String),
|
||||||
|
GeneratePassword,
|
||||||
|
// TOTP()
|
||||||
|
// Webauthn(response)
|
||||||
|
}
|
||||||
|
|
||||||
/* Recycle Requests area */
|
/* Recycle Requests area */
|
||||||
|
|
||||||
// Only two actions on recycled is possible. Search and Revive.
|
// Only two actions on recycled is possible. Search and Revive.
|
||||||
|
|
|
@ -10,8 +10,11 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
kanidm_client = { path = "../kanidm_client" }
|
kanidm_client = { path = "../kanidm_client" }
|
||||||
|
kanidm_proto = { path = "../kanidm_proto" }
|
||||||
rpassword = "0.4"
|
rpassword = "0.4"
|
||||||
structopt = { version = "0.2", default-features = false }
|
structopt = { version = "0.2", default-features = false }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.6"
|
env_logger = "0.6"
|
||||||
|
serde = "1.0"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
|
10
kanidm_tools/example.create.account.json
Normal file
10
kanidm_tools/example.create.account.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": ["idm_admin"],
|
||||||
|
"class": ["account"],
|
||||||
|
"displayname": ["IDM Admin"],
|
||||||
|
"description": ["Default IDM Admin"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
6
kanidm_tools/example.create.group.json
Normal file
6
kanidm_tools/example.create.group.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": ["demo_group"],
|
||||||
|
"class": ["group"]
|
||||||
|
}
|
||||||
|
]
|
5
kanidm_tools/example.modify.idm_admin.json
Normal file
5
kanidm_tools/example.modify.idm_admin.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
{"Present": ["member", "idm_admin"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
4
kanidm_tools/example.modify.json
Normal file
4
kanidm_tools/example.modify.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[
|
||||||
|
{ "Purged": "name" },
|
||||||
|
{ "Present": ["name", "demo_group"] }
|
||||||
|
]
|
|
@ -1,7 +1,16 @@
|
||||||
extern crate structopt;
|
extern crate structopt;
|
||||||
use kanidm_client::KanidmClient;
|
use kanidm_client::KanidmClient;
|
||||||
|
use kanidm_proto::v1::{Entry, Filter, Modify, ModifyList};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use structopt::StructOpt;
|
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;
|
extern crate env_logger;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
@ -40,25 +49,87 @@ impl CommonOpt {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
struct SearchOpt {
|
struct FilterOpt {
|
||||||
#[structopt()]
|
#[structopt()]
|
||||||
filter: String,
|
filter: String,
|
||||||
#[structopt(flatten)]
|
#[structopt(flatten)]
|
||||||
commonopts: CommonOpt,
|
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)]
|
#[derive(Debug, StructOpt)]
|
||||||
enum AccountOpt {
|
enum AccountOpt {
|
||||||
|
#[structopt(name = "credential")]
|
||||||
|
Credential(AccountCredential),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, StructOpt)]
|
||||||
|
enum SelfOpt {
|
||||||
|
#[structopt(name = "whoami")]
|
||||||
|
Whoami(CommonOpt),
|
||||||
#[structopt(name = "set_password")]
|
#[structopt(name = "set_password")]
|
||||||
SetPassword(CommonOpt),
|
SetPassword(CommonOpt),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, StructOpt)]
|
#[derive(Debug, StructOpt)]
|
||||||
enum ClientOpt {
|
enum ClientOpt {
|
||||||
#[structopt(name = "search")]
|
#[structopt(name = "raw")]
|
||||||
Search(SearchOpt),
|
Raw(RawOpt),
|
||||||
#[structopt(name = "whoami")]
|
#[structopt(name = "self")]
|
||||||
Whoami(CommonOpt),
|
CSelf(SelfOpt),
|
||||||
#[structopt(name = "account")]
|
#[structopt(name = "account")]
|
||||||
Account(AccountOpt),
|
Account(AccountOpt),
|
||||||
}
|
}
|
||||||
|
@ -66,15 +137,31 @@ enum ClientOpt {
|
||||||
impl ClientOpt {
|
impl ClientOpt {
|
||||||
fn debug(&self) -> bool {
|
fn debug(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ClientOpt::Whoami(copt) => copt.debug,
|
ClientOpt::Raw(ropt) => match ropt {
|
||||||
ClientOpt::Search(sopt) => sopt.commonopts.debug,
|
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 {
|
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() {
|
fn main() {
|
||||||
let opt = ClientOpt::from_args();
|
let opt = ClientOpt::from_args();
|
||||||
|
|
||||||
|
@ -86,31 +173,69 @@ fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
match opt {
|
match opt {
|
||||||
ClientOpt::Whoami(copt) => {
|
ClientOpt::Raw(ropt) => match ropt {
|
||||||
let client = copt.to_client();
|
RawOpt::Search(sopt) => {
|
||||||
|
let client = sopt.commonopts.to_client();
|
||||||
|
|
||||||
match client.whoami() {
|
let filter: Filter = serde_json::from_str(sopt.filter.as_str()).unwrap();
|
||||||
Ok(o_ent) => match o_ent {
|
let rset = client.search(filter).unwrap();
|
||||||
Some((ent, uat)) => {
|
|
||||||
debug!("{:?}", ent);
|
rset.iter().for_each(|e| {
|
||||||
println!("{}", uat);
|
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!("Unauthenticated"),
|
None => {
|
||||||
},
|
println!("Must provide a file");
|
||||||
Err(e) => println!("Error: {:?}", e),
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
RawOpt::Modify(mopt) => {
|
||||||
ClientOpt::Search(sopt) => {
|
let client = mopt.commonopts.to_client();
|
||||||
let client = sopt.commonopts.to_client();
|
// Read the file?
|
||||||
|
match mopt.file {
|
||||||
let rset = client.search_str(sopt.filter.as_str()).unwrap();
|
Some(p) => {
|
||||||
|
let filter: Filter = serde_json::from_str(mopt.filter.as_str()).unwrap();
|
||||||
for e in rset {
|
let r_list: Vec<Modify> = read_file(p).unwrap();
|
||||||
println!("{:?}", e);
|
let modlist = ModifyList::new_list(r_list);
|
||||||
|
client.modify(filter, modlist).unwrap()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("Must provide a file");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
RawOpt::Delete(dopt) => {
|
||||||
ClientOpt::Account(aopt) => match aopt {
|
let client = dopt.commonopts.to_client();
|
||||||
AccountOpt::SetPassword(copt) => {
|
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() {
|
||||||
|
Ok(o_ent) => match o_ent {
|
||||||
|
Some((ent, uat)) => {
|
||||||
|
debug!("{:?}", ent);
|
||||||
|
println!("{}", uat);
|
||||||
|
}
|
||||||
|
None => println!("Unauthenticated"),
|
||||||
|
},
|
||||||
|
Err(e) => println!("Error: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelfOpt::SetPassword(copt) => {
|
||||||
let client = copt.to_client();
|
let client = copt.to_client();
|
||||||
|
|
||||||
let password = rpassword::prompt_password_stderr("Enter new password: ").unwrap();
|
let password = rpassword::prompt_password_stderr("Enter new password: ").unwrap();
|
||||||
|
@ -118,5 +243,37 @@ fn main() {
|
||||||
client.idm_account_set_password(password).unwrap();
|
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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -555,7 +555,9 @@ pub trait AccessControlsTransaction {
|
||||||
})
|
})
|
||||||
.collect();
|
.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
|
// Get the set of attributes requested by the caller
|
||||||
// TODO #69: This currently
|
// TODO #69: This currently
|
||||||
|
@ -684,7 +686,9 @@ pub trait AccessControlsTransaction {
|
||||||
})
|
})
|
||||||
.collect();
|
.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"
|
// build two sets of "requested pres" and "requested rem"
|
||||||
let requested_pres: BTreeSet<&str> = me
|
let requested_pres: BTreeSet<&str> = me
|
||||||
|
@ -1020,7 +1024,9 @@ pub trait AccessControlsTransaction {
|
||||||
})
|
})
|
||||||
.collect();
|
.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
|
// For each entry
|
||||||
let r = entries.iter().fold(true, |acc, e| {
|
let r = entries.iter().fold(true, |acc, e| {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::event::{
|
||||||
AuthEvent, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
AuthEvent, CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
||||||
SearchEvent, SearchResult, WhoamiResult,
|
SearchEvent, SearchResult, WhoamiResult,
|
||||||
};
|
};
|
||||||
use crate::idm::event::PasswordChangeEvent;
|
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::OperationError;
|
||||||
|
|
||||||
use crate::filter::{Filter, FilterInvalid};
|
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::Entry as ProtoEntry;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AuthRequest, AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse,
|
AuthRequest, AuthResponse, CreateRequest, DeleteRequest, ModifyRequest, OperationResponse,
|
||||||
SearchRequest, SearchResponse, SingleStringRequest, UserAuthToken, WhoamiResponse,
|
SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||||
|
WhoamiResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
@ -160,6 +161,33 @@ impl Message for IdmAccountSetPasswordMessage {
|
||||||
type Result = Result<OperationResponse, OperationError>;
|
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 {
|
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.
|
// These below are internal only types.
|
||||||
|
|
||||||
impl Handler<PurgeTombstoneEvent> for QueryServerV1 {
|
impl Handler<PurgeTombstoneEvent> for QueryServerV1 {
|
||||||
|
|
|
@ -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.
|
// 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 _UUID_IDM_HIGH_PRIVILEGE: &'static str = "00000000-0000-0000-0000-000000001000";
|
||||||
pub static JSON_IDM_HIGH_PRIVILEGE_V1: &'static str = r#"{
|
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",
|
"class",
|
||||||
"name",
|
"name",
|
||||||
"displayname",
|
"displayname",
|
||||||
|
"description",
|
||||||
"primary_credential",
|
"primary_credential",
|
||||||
"ssh_publickey"
|
"ssh_publickey"
|
||||||
],
|
],
|
||||||
|
@ -967,6 +980,37 @@ pub static JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1: &'static str = r#"{
|
||||||
|
|
||||||
// 21 - anonymous / everyone schema read.
|
// 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.
|
// Anonymous should be the last opbject in the range here.
|
||||||
pub static JSON_ANONYMOUS_V1: &'static str = r#"{
|
pub static JSON_ANONYMOUS_V1: &'static str = r#"{
|
||||||
"attrs": {
|
"attrs": {
|
||||||
|
|
|
@ -15,8 +15,9 @@ use crate::config::Configuration;
|
||||||
// SearchResult
|
// SearchResult
|
||||||
use crate::actors::v1::QueryServerV1;
|
use crate::actors::v1::QueryServerV1;
|
||||||
use crate::actors::v1::{
|
use crate::actors::v1::{
|
||||||
AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage, InternalSearchMessage,
|
AuthMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage,
|
||||||
ModifyMessage, SearchMessage, WhoamiMessage,
|
InternalCredentialSetMessage, InternalSearchMessage, ModifyMessage, SearchMessage,
|
||||||
|
WhoamiMessage,
|
||||||
};
|
};
|
||||||
use crate::async_log;
|
use crate::async_log;
|
||||||
use crate::audit::AuditScope;
|
use crate::audit::AuditScope;
|
||||||
|
@ -34,7 +35,7 @@ use crate::value::PartialValue;
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::OperationError;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest,
|
AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest,
|
||||||
SingleStringRequest, UserAuthToken,
|
SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -108,9 +109,9 @@ macro_rules! json_event_post {
|
||||||
"Json Decode Failed: {:?}",
|
"Json Decode Failed: {:?}",
|
||||||
e
|
e
|
||||||
)))),
|
)))),
|
||||||
}
|
} // end match
|
||||||
},
|
}, // end closure
|
||||||
)
|
) // end and_then
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +219,69 @@ fn json_rest_event_get_id(
|
||||||
Box::new(res)
|
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(
|
fn schema_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||||
|
@ -314,6 +378,13 @@ fn account_get_id(
|
||||||
json_rest_event_get_id(path, req, state, filter)
|
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(
|
fn group_get(
|
||||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
) -> 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)
|
r.method(http::Method::GET).with(do_nothing)
|
||||||
// add delete
|
// 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| {
|
.resource("/v1/account/{id}/_credential/{cid}/_lock", |r| {
|
||||||
r.method(http::Method::GET).with(do_nothing)
|
r.method(http::Method::GET).with(do_nothing)
|
||||||
// add post, delete
|
// add post, delete
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::server::QueryServerWriteTransaction;
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use kanidm_proto::v1::OperationError;
|
use kanidm_proto::v1::{OperationError, UserAuthToken};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PasswordChangeEvent {
|
pub struct PasswordChangeEvent {
|
||||||
|
@ -40,4 +40,47 @@ impl PasswordChangeEvent {
|
||||||
appid: None,
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ use crate::constants::AUTH_SESSION_TIMEOUT;
|
||||||
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
use crate::event::{AuthEvent, AuthEventStep, AuthResult};
|
||||||
use crate::idm::account::Account;
|
use crate::idm::account::Account;
|
||||||
use crate::idm::authsession::AuthSession;
|
use crate::idm::authsession::AuthSession;
|
||||||
use crate::idm::event::PasswordChangeEvent;
|
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent};
|
||||||
use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction};
|
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 crate::value::PartialValue;
|
||||||
|
|
||||||
use kanidm_proto::v1::AuthState;
|
use kanidm_proto::v1::AuthState;
|
||||||
|
@ -282,6 +282,49 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||||
self.set_account_password(au, &pce)
|
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> {
|
pub fn commit(self, au: &mut AuditScope) -> Result<(), OperationError> {
|
||||||
self.qs_write.commit(au)
|
self.qs_write.commit(au)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1583,6 +1583,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
||||||
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_GROUP_WRITE_PRIV_V1,
|
JSON_IDM_GROUP_WRITE_PRIV_V1,
|
||||||
|
JSON_IDM_GROUP_CREATE_PRIV_V1,
|
||||||
JSON_IDM_ACCOUNT_WRITE_PRIV_V1,
|
JSON_IDM_ACCOUNT_WRITE_PRIV_V1,
|
||||||
JSON_IDM_ACCOUNT_READ_PRIV_V1,
|
JSON_IDM_ACCOUNT_READ_PRIV_V1,
|
||||||
JSON_IDM_RADIUS_SERVERS_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_ATTRS_PRIV_V1,
|
||||||
JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1,
|
JSON_IDM_ACP_SCHEMA_WRITE_CLASSES_PRIV_V1,
|
||||||
JSON_IDM_ACP_ACP_MANAGER_PRIV_V1,
|
JSON_IDM_ACP_ACP_MANAGER_PRIV_V1,
|
||||||
|
JSON_IDM_ACP_GROUP_CREATE_V1,
|
||||||
];
|
];
|
||||||
|
|
||||||
let res: Result<(), _> = idm_entries
|
let res: Result<(), _> = idm_entries
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use uuid::{Builder, Uuid};
|
use uuid::{Builder, Uuid};
|
||||||
|
|
||||||
|
use rand::distributions::Alphanumeric;
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
|
||||||
pub type SID = [u8; 4];
|
pub type SID = [u8; 4];
|
||||||
|
|
||||||
fn uuid_from_u64_u32(a: u64, b: u32, sid: &SID) -> Uuid {
|
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)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::utils::uuid_from_duration;
|
use crate::utils::uuid_from_duration;
|
||||||
|
|
Loading…
Reference in a new issue