mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-22 00:43:54 +02:00
129 pam nsswitch stage 1 daemon (#179)
Implements #129, pam and nsswitch daemon capability. This is stage 1, which adds a localhost unix domain socket resolver, a ssh key client, support to the server for generating unix tokens, an async client lib, and client handles for adding posix extensions to accounts and groups.
This commit is contained in:
parent
e41fada28a
commit
d063d358ad
|
@ -5,5 +5,6 @@ members = [
|
|||
"kanidmd",
|
||||
"kanidm_client",
|
||||
"kanidm_tools",
|
||||
"kanidm_unix_int",
|
||||
]
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ repository = "https://github.com/kanidm/kanidm/"
|
|||
[dependencies]
|
||||
log = "0.4"
|
||||
env_logger = "0.6"
|
||||
reqwest = "0.9"
|
||||
reqwest = { version = "0.10", features=["blocking", "cookies", "json", "native-tls"] }
|
||||
kanidm_proto = { path = "../kanidm_proto", version = "0.1" }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
|
|
130
kanidm_client/src/asynchronous.rs
Normal file
130
kanidm_client/src/asynchronous.rs
Normal file
|
@ -0,0 +1,130 @@
|
|||
use crate::{ClientError, KanidmClientBuilder};
|
||||
use reqwest;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use kanidm_proto::v1::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KanidmAsyncClient {
|
||||
pub(crate) client: reqwest::Client,
|
||||
pub(crate) addr: String,
|
||||
pub(crate) builder: KanidmClientBuilder,
|
||||
}
|
||||
|
||||
impl KanidmAsyncClient {
|
||||
async fn perform_post_request<R: Serialize, T: DeserializeOwned>(
|
||||
&self,
|
||||
dest: &str,
|
||||
request: R,
|
||||
) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// format doesn't work in async ?!
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
|
||||
let req_string = serde_json::to_string(&request).unwrap();
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(dest.as_str())
|
||||
.body(req_string)
|
||||
.send()
|
||||
.await
|
||||
.map_err(ClientError::Transport)?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect, response.json().await.ok())),
|
||||
}
|
||||
|
||||
// TODO: What about errors
|
||||
let r: T = response.json().await.unwrap();
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
async fn perform_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
|
||||
let dest = [self.addr.as_str(), dest].concat();
|
||||
debug!("{:?}", dest);
|
||||
// let dest = format!("{}{}", self.addr, dest);
|
||||
let response = self
|
||||
.client
|
||||
.get(dest.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(ClientError::Transport)?;
|
||||
|
||||
match response.status() {
|
||||
reqwest::StatusCode::OK => {}
|
||||
unexpect => return Err(ClientError::Http(unexpect, response.json().await.ok())),
|
||||
}
|
||||
|
||||
// TODO: What about errors
|
||||
let r: T = response.json().await.unwrap();
|
||||
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
pub async fn auth_step_init(
|
||||
&self,
|
||||
ident: &str,
|
||||
appid: Option<&str>,
|
||||
) -> Result<AuthState, ClientError> {
|
||||
let auth_init = AuthRequest {
|
||||
step: AuthStep::Init(ident.to_string(), appid.map(|s| s.to_string())),
|
||||
};
|
||||
|
||||
let r: Result<AuthResponse, _> = self.perform_post_request("/v1/auth", auth_init).await;
|
||||
r.map(|v| v.state)
|
||||
}
|
||||
|
||||
pub async fn auth_anonymous(&self) -> Result<UserAuthToken, ClientError> {
|
||||
// TODO: Check state for auth continue contains anonymous.
|
||||
let _state = match self.auth_step_init("anonymous", None).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let auth_anon = AuthRequest {
|
||||
step: AuthStep::Creds(vec![AuthCredential::Anonymous]),
|
||||
};
|
||||
let r: Result<AuthResponse, _> = self.perform_post_request("/v1/auth", auth_anon).await;
|
||||
|
||||
let r = r?;
|
||||
|
||||
match r.state {
|
||||
AuthState::Success(uat) => {
|
||||
debug!("==> Authed as uat; {:?}", uat);
|
||||
Ok(uat)
|
||||
}
|
||||
_ => Err(ClientError::AuthenticationFailed),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
||||
let whoami_dest = [self.addr.as_str(), "/v1/self"].concat();
|
||||
// format!("{}/v1/self", self.addr);
|
||||
debug!("{:?}", whoami_dest);
|
||||
let response = self.client.get(whoami_dest.as_str()).send().await.unwrap();
|
||||
|
||||
match response.status() {
|
||||
// Continue to process.
|
||||
reqwest::StatusCode::OK => {}
|
||||
reqwest::StatusCode::UNAUTHORIZED => return Ok(None),
|
||||
unexpect => return Err(ClientError::Http(unexpect, response.json().await.ok())),
|
||||
}
|
||||
|
||||
let r: WhoamiResponse =
|
||||
serde_json::from_str(response.text().await.unwrap().as_str()).unwrap();
|
||||
|
||||
Ok(Some((r.youare, r.uat)))
|
||||
}
|
||||
|
||||
pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
|
||||
// Format doesn't work in async
|
||||
// format!("/v1/account/{}/_unix/_token", id).as_str()
|
||||
self.perform_get_request(["/v1/account/", id, "/_unix/_token"].concat().as_str())
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -12,16 +12,22 @@ use std::collections::BTreeMap;
|
|||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use toml;
|
||||
|
||||
use kanidm_proto::v1::{
|
||||
AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep, CreateRequest, DeleteRequest,
|
||||
Entry, Filter, ModifyList, ModifyRequest, OperationError, OperationResponse, RadiusAuthToken,
|
||||
SearchRequest, SearchResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||
AccountUnixExtend, AuthCredential, AuthRequest, AuthResponse, AuthState, AuthStep,
|
||||
CreateRequest, DeleteRequest, Entry, Filter, GroupUnixExtend, ModifyList, ModifyRequest,
|
||||
OperationError, OperationResponse, RadiusAuthToken, SearchRequest, SearchResponse,
|
||||
SetAuthCredential, SingleStringRequest, UnixGroupToken, UnixUserToken, UserAuthToken,
|
||||
WhoamiResponse,
|
||||
};
|
||||
use serde_json;
|
||||
|
||||
pub mod asynchronous;
|
||||
|
||||
use crate::asynchronous::KanidmAsyncClient;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
Unauthorized,
|
||||
|
@ -48,6 +54,7 @@ pub struct KanidmClientBuilder {
|
|||
verify_ca: bool,
|
||||
verify_hostnames: bool,
|
||||
ca: Option<reqwest::Certificate>,
|
||||
connect_timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl KanidmClientBuilder {
|
||||
|
@ -57,6 +64,7 @@ impl KanidmClientBuilder {
|
|||
verify_ca: true,
|
||||
verify_hostnames: true,
|
||||
ca: None,
|
||||
connect_timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,6 +82,7 @@ impl KanidmClientBuilder {
|
|||
verify_ca,
|
||||
verify_hostnames,
|
||||
ca,
|
||||
connect_timeout,
|
||||
} = self;
|
||||
// Process and apply all our options if they exist.
|
||||
let address = match kcc.uri {
|
||||
|
@ -92,6 +101,7 @@ impl KanidmClientBuilder {
|
|||
verify_ca,
|
||||
verify_hostnames,
|
||||
ca,
|
||||
connect_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -124,6 +134,7 @@ impl KanidmClientBuilder {
|
|||
verify_ca: self.verify_ca,
|
||||
verify_hostnames: self.verify_hostnames,
|
||||
ca: self.ca,
|
||||
connect_timeout: self.connect_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,6 +145,7 @@ impl KanidmClientBuilder {
|
|||
// We have to flip the bool state here due to english language.
|
||||
verify_hostnames: !accept_invalid_hostnames,
|
||||
ca: self.ca,
|
||||
connect_timeout: self.connect_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,6 +156,17 @@ impl KanidmClientBuilder {
|
|||
verify_ca: !accept_invalid_certs,
|
||||
verify_hostnames: self.verify_hostnames,
|
||||
ca: self.ca,
|
||||
connect_timeout: self.connect_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_timeout(self, secs: u64) -> Self {
|
||||
KanidmClientBuilder {
|
||||
address: self.address,
|
||||
verify_ca: self.verify_ca,
|
||||
verify_hostnames: self.verify_hostnames,
|
||||
ca: self.ca,
|
||||
connect_timeout: Some(secs),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,6 +179,7 @@ impl KanidmClientBuilder {
|
|||
verify_ca: self.verify_ca,
|
||||
verify_hostnames: self.verify_hostnames,
|
||||
ca: Some(ca),
|
||||
connect_timeout: self.connect_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -170,6 +194,40 @@ impl KanidmClientBuilder {
|
|||
}
|
||||
};
|
||||
|
||||
let client_builder = reqwest::blocking::Client::builder()
|
||||
.cookie_store(true)
|
||||
.danger_accept_invalid_hostnames(!self.verify_hostnames)
|
||||
.danger_accept_invalid_certs(!self.verify_ca);
|
||||
|
||||
let client_builder = match &self.ca {
|
||||
Some(cert) => client_builder.add_root_certificate(cert.clone()),
|
||||
None => client_builder,
|
||||
};
|
||||
|
||||
let client_builder = match &self.connect_timeout {
|
||||
Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
|
||||
None => client_builder,
|
||||
};
|
||||
|
||||
let client = client_builder.build()?;
|
||||
|
||||
Ok(KanidmClient {
|
||||
client,
|
||||
addr: address,
|
||||
builder: self,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_async(self) -> Result<KanidmAsyncClient, reqwest::Error> {
|
||||
// Errghh, how to handle this cleaner.
|
||||
let address = match &self.address {
|
||||
Some(a) => a.clone(),
|
||||
None => {
|
||||
eprintln!("uri (-H) missing, can not proceed");
|
||||
unimplemented!();
|
||||
}
|
||||
};
|
||||
|
||||
let client_builder = reqwest::Client::builder()
|
||||
.cookie_store(true)
|
||||
.danger_accept_invalid_hostnames(!self.verify_hostnames)
|
||||
|
@ -180,9 +238,14 @@ impl KanidmClientBuilder {
|
|||
None => client_builder,
|
||||
};
|
||||
|
||||
let client_builder = match &self.connect_timeout {
|
||||
Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
|
||||
None => client_builder,
|
||||
};
|
||||
|
||||
let client = client_builder.build()?;
|
||||
|
||||
Ok(KanidmClient {
|
||||
Ok(KanidmAsyncClient {
|
||||
client,
|
||||
addr: address,
|
||||
builder: self,
|
||||
|
@ -192,7 +255,7 @@ impl KanidmClientBuilder {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct KanidmClient {
|
||||
client: reqwest::Client,
|
||||
client: reqwest::blocking::Client,
|
||||
addr: String,
|
||||
builder: KanidmClientBuilder,
|
||||
}
|
||||
|
@ -225,7 +288,7 @@ impl KanidmClient {
|
|||
|
||||
let req_string = serde_json::to_string(&request).unwrap();
|
||||
|
||||
let mut response = self
|
||||
let response = self
|
||||
.client
|
||||
.post(dest.as_str())
|
||||
.body(req_string)
|
||||
|
@ -252,7 +315,7 @@ impl KanidmClient {
|
|||
|
||||
let req_string = serde_json::to_string(&request).unwrap();
|
||||
|
||||
let mut response = self
|
||||
let response = self
|
||||
.client
|
||||
.put(dest.as_str())
|
||||
.body(req_string)
|
||||
|
@ -272,7 +335,7 @@ impl KanidmClient {
|
|||
|
||||
fn perform_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
|
||||
let dest = format!("{}{}", self.addr, dest);
|
||||
let mut response = self
|
||||
let response = self
|
||||
.client
|
||||
.get(dest.as_str())
|
||||
.send()
|
||||
|
@ -291,7 +354,7 @@ impl KanidmClient {
|
|||
|
||||
fn perform_delete_request(&self, dest: &str) -> Result<(), ClientError> {
|
||||
let dest = format!("{}{}", self.addr, dest);
|
||||
let mut response = self
|
||||
let response = self
|
||||
.client
|
||||
.delete(dest.as_str())
|
||||
.send()
|
||||
|
@ -309,7 +372,7 @@ impl KanidmClient {
|
|||
// Can't use generic get due to possible un-auth case.
|
||||
pub fn whoami(&self) -> Result<Option<(Entry, UserAuthToken)>, ClientError> {
|
||||
let whoami_dest = format!("{}/v1/self", self.addr);
|
||||
let mut response = self.client.get(whoami_dest.as_str()).send().unwrap();
|
||||
let response = self.client.get(whoami_dest.as_str()).send().unwrap();
|
||||
|
||||
match response.status() {
|
||||
// Continue to process.
|
||||
|
@ -456,6 +519,21 @@ impl KanidmClient {
|
|||
self.perform_delete_request(format!("/v1/group/{}/_attr/member", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_group_unix_token_get(&self, id: &str) -> Result<UnixGroupToken, ClientError> {
|
||||
self.perform_get_request(format!("/v1/group/{}/_unix/_token", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_group_unix_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
gidnumber: Option<u32>,
|
||||
) -> Result<(), ClientError> {
|
||||
let gx = GroupUnixExtend {
|
||||
gidnumber: gidnumber,
|
||||
};
|
||||
self.perform_post_request(format!("/v1/group/{}/_unix", id).as_str(), gx)
|
||||
}
|
||||
|
||||
pub fn idm_group_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(format!("/v1/group/{}", id).as_str())
|
||||
}
|
||||
|
@ -555,6 +633,23 @@ impl KanidmClient {
|
|||
self.perform_get_request(format!("/v1/account/{}/_radius/_token", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_extend(
|
||||
&self,
|
||||
id: &str,
|
||||
gidnumber: Option<u32>,
|
||||
shell: Option<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
let ux = AccountUnixExtend {
|
||||
shell: shell.map(|s| s.to_string()),
|
||||
gidnumber: gidnumber,
|
||||
};
|
||||
self.perform_post_request(format!("/v1/account/{}/_unix", id).as_str(), ux)
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_unix/_token", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_get_ssh_pubkeys(&self, id: &str) -> Result<Vec<String>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_ssh_pubkeys", id).as_str())
|
||||
}
|
||||
|
|
|
@ -539,6 +539,79 @@ fn test_server_rest_domain_lifecycle() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_rest_posix_lifecycle() {
|
||||
run_test(|rsclient: KanidmClient| {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", vec!["admin"])
|
||||
.unwrap();
|
||||
|
||||
// Create a new account
|
||||
rsclient
|
||||
.idm_account_create("posix_account", "Posix Demo Account")
|
||||
.unwrap();
|
||||
|
||||
// Extend the account with posix attrs.
|
||||
rsclient
|
||||
.idm_account_unix_extend("posix_account", None, None)
|
||||
.unwrap();
|
||||
|
||||
// Create a group
|
||||
|
||||
// Extend the group with posix attrs
|
||||
rsclient.idm_group_create("posix_group").unwrap();
|
||||
rsclient
|
||||
.idm_group_add_members("posix_group", vec!["posix_account"])
|
||||
.unwrap();
|
||||
rsclient.idm_group_unix_extend("posix_group", None).unwrap();
|
||||
|
||||
// Open a new connection as anonymous
|
||||
let res = rsclient.auth_anonymous();
|
||||
assert!(res.is_ok());
|
||||
|
||||
// Get the account by name
|
||||
let r = rsclient
|
||||
.idm_account_unix_token_get("posix_account")
|
||||
.unwrap();
|
||||
// Get the account by gidnumber
|
||||
let r1 = rsclient
|
||||
.idm_account_unix_token_get(r.gidnumber.to_string().as_str())
|
||||
.unwrap();
|
||||
// get the account by spn
|
||||
let r2 = rsclient.idm_account_unix_token_get(r.spn.as_str()).unwrap();
|
||||
// get the account by uuid
|
||||
let r3 = rsclient
|
||||
.idm_account_unix_token_get(r.uuid.as_str())
|
||||
.unwrap();
|
||||
|
||||
println!("{:?}", r);
|
||||
assert!(r.name == "posix_account");
|
||||
assert!(r1.name == "posix_account");
|
||||
assert!(r2.name == "posix_account");
|
||||
assert!(r3.name == "posix_account");
|
||||
|
||||
// get the group by nam
|
||||
let r = rsclient.idm_group_unix_token_get("posix_group").unwrap();
|
||||
// Get the group by gidnumber
|
||||
let r1 = rsclient
|
||||
.idm_group_unix_token_get(r.gidnumber.to_string().as_str())
|
||||
.unwrap();
|
||||
// get the group spn
|
||||
let r2 = rsclient.idm_group_unix_token_get(r.spn.as_str()).unwrap();
|
||||
// get the group by uuid
|
||||
let r3 = rsclient.idm_group_unix_token_get(r.uuid.as_str()).unwrap();
|
||||
|
||||
println!("{:?}", r);
|
||||
assert!(r.name == "posix_group");
|
||||
assert!(r1.name == "posix_group");
|
||||
assert!(r2.name == "posix_group");
|
||||
assert!(r3.name == "posix_group");
|
||||
});
|
||||
}
|
||||
|
||||
// Test the self version of the radius path.
|
||||
|
||||
// Test hitting all auth-required endpoints and assert they give unauthorized.
|
||||
|
|
|
@ -176,6 +176,37 @@ impl fmt::Display for RadiusAuthToken {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UnixGroupToken {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub uuid: String,
|
||||
pub gidnumber: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct GroupUnixExtend {
|
||||
pub gidnumber: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UnixUserToken {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub displayname: String,
|
||||
pub gidnumber: u32,
|
||||
pub uuid: String,
|
||||
pub shell: Option<String>,
|
||||
pub groups: Vec<UnixGroupToken>,
|
||||
pub sshkeys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AccountUnixExtend {
|
||||
pub gidnumber: Option<u32>,
|
||||
pub shell: Option<String>,
|
||||
}
|
||||
|
||||
/* ===== low level proto types ===== */
|
||||
|
||||
// ProtoEntry vs Entry
|
||||
|
|
|
@ -15,7 +15,7 @@ name = "kanidm"
|
|||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "kanidm_ssh_authorizedkeys"
|
||||
name = "kanidm_ssh_authorizedkeys_direct"
|
||||
path = "src/ssh_authorizedkeys.rs"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -183,6 +183,26 @@ enum AccountRadius {
|
|||
Delete(AccountNamedOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct AccountPosixOpt {
|
||||
#[structopt(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[structopt(long = "gidnumber")]
|
||||
gidnumber: Option<u32>,
|
||||
#[structopt(long = "shell")]
|
||||
shell: Option<String>,
|
||||
#[structopt(flatten)]
|
||||
copt: CommonOpt,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
enum AccountPosix {
|
||||
#[structopt(name = "show")]
|
||||
Show(AccountNamedOpt),
|
||||
#[structopt(name = "set")]
|
||||
Set(AccountPosixOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
enum AccountSsh {
|
||||
#[structopt(name = "list_publickeys")]
|
||||
|
@ -199,6 +219,8 @@ enum AccountOpt {
|
|||
Credential(AccountCredential),
|
||||
#[structopt(name = "radius")]
|
||||
Radius(AccountRadius),
|
||||
#[structopt(name = "posix")]
|
||||
Posix(AccountPosix),
|
||||
#[structopt(name = "ssh")]
|
||||
Ssh(AccountSsh),
|
||||
#[structopt(name = "list")]
|
||||
|
@ -290,6 +312,10 @@ impl ClientOpt {
|
|||
AccountRadius::Generate(aro) => aro.copt.debug,
|
||||
AccountRadius::Delete(aro) => aro.copt.debug,
|
||||
},
|
||||
AccountOpt::Posix(apopt) => match apopt {
|
||||
AccountPosix::Show(apo) => apo.copt.debug,
|
||||
AccountPosix::Set(apo) => apo.copt.debug,
|
||||
},
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(ano) => ano.copt.debug,
|
||||
AccountSsh::Add(ano) => ano.copt.debug,
|
||||
|
@ -459,6 +485,25 @@ fn main() {
|
|||
.unwrap();
|
||||
}
|
||||
}, // end AccountOpt::Radius
|
||||
AccountOpt::Posix(apopt) => match apopt {
|
||||
AccountPosix::Show(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let token = client
|
||||
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
|
||||
.unwrap();
|
||||
println!("{:?}", token);
|
||||
}
|
||||
AccountPosix::Set(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
client
|
||||
.idm_account_unix_extend(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
aopt.gidnumber,
|
||||
aopt.shell.as_deref(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}, // end AccountOpt::Posix
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
|
|
49
kanidm_unix_int/Cargo.toml
Normal file
49
kanidm_unix_int/Cargo.toml
Normal file
|
@ -0,0 +1,49 @@
|
|||
[package]
|
||||
name = "kanidm_unix_int"
|
||||
version = "0.1.0"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
description = "Kanidm Unix Integration Clients"
|
||||
documentation = "https://docs.rs/kanidm/latest/kanidm/"
|
||||
homepage = "https://github.com/kanidm/kanidm/"
|
||||
repository = "https://github.com/kanidm/kanidm/"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "kanidm_unix_common"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "kanidm_unixd"
|
||||
path = "src/daemon.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "kanidm_ssh_authorizedkeys"
|
||||
path = "src/ssh_authorizedkeys.rs"
|
||||
|
||||
[dependencies]
|
||||
kanidm_client = { path = "../kanidm_client", version = "0.1" }
|
||||
kanidm_proto = { path = "../kanidm_proto", version = "0.1" }
|
||||
# actix = { path = "../../actix", version = "0.9" }
|
||||
actix = "0.7"
|
||||
# actix-rt = "1.0"
|
||||
tokio = { version = "0.2", features=["full"] }
|
||||
tokio-util = { version = "0.2", features = ["codec"] }
|
||||
futures = "0.3"
|
||||
bytes = "0.5"
|
||||
|
||||
log = "0.4"
|
||||
env_logger = "0.6"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_cbor = "0.10"
|
||||
structopt = { version = "0.2", default-features = false }
|
||||
|
||||
rusqlite = { version = "0.20", features = ["backup"] }
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
kanidm = { path = "../kanidmd", version = "0.1" }
|
235
kanidm_unix_int/src/cache.rs
Normal file
235
kanidm_unix_int/src/cache.rs
Normal file
|
@ -0,0 +1,235 @@
|
|||
use crate::db::Db;
|
||||
use kanidm_client::asynchronous::KanidmAsyncClient;
|
||||
use kanidm_client::ClientError;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
use std::ops::Add;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum CacheState {
|
||||
Online,
|
||||
Offline,
|
||||
OfflineNextCheck(SystemTime),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CacheLayer {
|
||||
db: Db,
|
||||
client: KanidmAsyncClient,
|
||||
state: Mutex<CacheState>,
|
||||
timeout_seconds: u64,
|
||||
}
|
||||
|
||||
impl CacheLayer {
|
||||
pub fn new(
|
||||
// need db path
|
||||
path: &str,
|
||||
// cache timeout
|
||||
timeout_seconds: u64,
|
||||
//
|
||||
client: KanidmAsyncClient,
|
||||
) -> Result<Self, ()> {
|
||||
let db = Db::new(path)?;
|
||||
|
||||
// setup and do a migrate.
|
||||
{
|
||||
let dbtxn = db.write();
|
||||
dbtxn.migrate()?;
|
||||
dbtxn.commit()?;
|
||||
}
|
||||
|
||||
// We assume we are offline at start up, and we mark the next "online check" as
|
||||
// being valid from "now".
|
||||
Ok(CacheLayer {
|
||||
db: db,
|
||||
client: client,
|
||||
state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())),
|
||||
timeout_seconds: timeout_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_cachestate(&self) -> CacheState {
|
||||
let g = self.state.lock().await;
|
||||
(*g).clone()
|
||||
}
|
||||
|
||||
async fn set_cachestate(&self, state: CacheState) {
|
||||
let mut g = self.state.lock().await;
|
||||
*g = state;
|
||||
}
|
||||
|
||||
// Need a way to mark online/offline.
|
||||
pub async fn attempt_online(&self) {
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(SystemTime::now()))
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn mark_offline(&self) {
|
||||
self.set_cachestate(CacheState::Offline).await;
|
||||
}
|
||||
|
||||
// Invalidate the whole cache. We do this by just deleting the content
|
||||
// of the sqlite db.
|
||||
pub fn invalidate(&self) -> Result<(), ()> {
|
||||
let dbtxn = self.db.write();
|
||||
dbtxn.clear_cache().and_then(|_| dbtxn.commit())
|
||||
}
|
||||
|
||||
fn get_cached_usertoken(&self, account_id: &str) -> Result<(bool, Option<UnixUserToken>), ()> {
|
||||
// Account_id could be:
|
||||
// * gidnumber
|
||||
// * name
|
||||
// * spn
|
||||
// * uuid
|
||||
// Attempt to search these in the db.
|
||||
let dbtxn = self.db.write();
|
||||
let r = dbtxn.get_account(account_id)?;
|
||||
|
||||
match r {
|
||||
Some((ut, ex)) => {
|
||||
// Are we expired?
|
||||
let offset = Duration::from_secs(ex);
|
||||
let ex_time = SystemTime::UNIX_EPOCH + offset;
|
||||
let now = SystemTime::now();
|
||||
|
||||
if now >= ex_time {
|
||||
Ok((true, Some(ut)))
|
||||
} else {
|
||||
Ok((false, Some(ut)))
|
||||
}
|
||||
}
|
||||
None => Ok((true, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cache_usertoken(&self, token: &UnixUserToken) -> Result<(), ()> {
|
||||
// Set an expiry
|
||||
let ex_time = SystemTime::now() + Duration::from_secs(self.timeout_seconds);
|
||||
let offset = ex_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_err(|e| {
|
||||
error!("time conversion error - ex_time less than epoch? {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
let dbtxn = self.db.write();
|
||||
dbtxn
|
||||
.update_account(token, offset.as_secs())
|
||||
.and_then(|_| dbtxn.commit())
|
||||
}
|
||||
|
||||
async fn refresh_usertoken(
|
||||
&self,
|
||||
account_id: &str,
|
||||
token: Option<UnixUserToken>,
|
||||
) -> Result<Option<UnixUserToken>, ()> {
|
||||
match self.client.idm_account_unix_token_get(account_id).await {
|
||||
Ok(n_tok) => {
|
||||
// We have the token!
|
||||
self.set_cache_usertoken(&n_tok)?;
|
||||
Ok(Some(n_tok))
|
||||
}
|
||||
Err(e) => {
|
||||
match e {
|
||||
ClientError::Transport(er) => {
|
||||
error!("transport error, moving to offline -> {:?}", er);
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
Ok(token)
|
||||
}
|
||||
er => {
|
||||
error!("client error -> {:?}", er);
|
||||
// Some other transient error, continue with the token.
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_usertoken(&self, account_id: &str) -> Result<Option<UnixUserToken>, ()> {
|
||||
debug!("get_usertoken");
|
||||
// get the item from the cache
|
||||
let (expired, item) = self.get_cached_usertoken(account_id).map_err(|e| {
|
||||
debug!("get_usertoken error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
let state = self.get_cachestate().await;
|
||||
|
||||
match (expired, state) {
|
||||
(_, CacheState::Offline) => {
|
||||
debug!("offline, returning cached item");
|
||||
Ok(item)
|
||||
}
|
||||
(false, CacheState::OfflineNextCheck(time)) => {
|
||||
debug!(
|
||||
"offline valid, next check {:?}, returning cached item",
|
||||
time
|
||||
);
|
||||
// Still valid within lifetime, return.
|
||||
Ok(item)
|
||||
}
|
||||
(false, CacheState::Online) => {
|
||||
debug!("online valid, returning cached item");
|
||||
// Still valid within lifetime, return.
|
||||
Ok(item)
|
||||
}
|
||||
(true, CacheState::OfflineNextCheck(time)) => {
|
||||
debug!("offline expired, next check {:?}, refresh cache", time);
|
||||
// Attempt to refresh the item
|
||||
// Return it.
|
||||
if SystemTime::now() >= time && self.test_connection().await {
|
||||
// We brought ourselves online, lets go
|
||||
self.refresh_usertoken(account_id, item).await
|
||||
} else {
|
||||
// Unable to bring up connection, return cache.
|
||||
Ok(item)
|
||||
}
|
||||
}
|
||||
(true, CacheState::Online) => {
|
||||
debug!("online expired, refresh cache");
|
||||
// Attempt to refresh the item
|
||||
// Return it.
|
||||
self.refresh_usertoken(account_id, item).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get ssh keys for an account id
|
||||
pub async fn get_sshkeys(&self, account_id: &str) -> Result<Vec<String>, ()> {
|
||||
let token = self.get_usertoken(account_id).await?;
|
||||
Ok(token.map(|t| t.sshkeys).unwrap_or_else(|| Vec::new()))
|
||||
}
|
||||
|
||||
pub async fn test_connection(&self) -> bool {
|
||||
let state = self.get_cachestate().await;
|
||||
match state {
|
||||
CacheState::Offline => {
|
||||
debug!("Offline -> no change");
|
||||
false
|
||||
}
|
||||
CacheState::OfflineNextCheck(_time) => match self.client.auth_anonymous().await {
|
||||
Ok(_uat) => {
|
||||
debug!("OfflineNextCheck -> authenticated");
|
||||
self.set_cachestate(CacheState::Online).await;
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("OfflineNextCheck -> disconnected, staying offline. {:?}", e);
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
false
|
||||
}
|
||||
},
|
||||
CacheState::Online => {
|
||||
debug!("Online, no change");
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
kanidm_unix_int/src/constants.rs
Normal file
4
kanidm_unix_int/src/constants.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub const DEFAULT_SOCK_PATH: &'static str = "/tmp/kanidm.sock";
|
||||
pub const DEFAULT_DB_PATH: &'static str = "/tmp/kanidm.cache.db";
|
||||
pub const DEFAULT_CONN_TIMEOUT: u64 = 2;
|
||||
pub const DEFAULT_CACHE_TIMEOUT: u64 = 15;
|
237
kanidm_unix_int/src/daemon.rs
Normal file
237
kanidm_unix_int/src/daemon.rs
Normal file
|
@ -0,0 +1,237 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio_util::codec::Framed;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use kanidm_client::KanidmClientBuilder;
|
||||
|
||||
use kanidm_unix_common::cache::CacheLayer;
|
||||
use kanidm_unix_common::constants::{
|
||||
DEFAULT_CACHE_TIMEOUT, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_SOCK_PATH,
|
||||
};
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
//=== the codec
|
||||
|
||||
struct ClientCodec;
|
||||
|
||||
impl Decoder for ClientCodec {
|
||||
type Item = ClientRequest;
|
||||
type Error = io::Error;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
match serde_cbor::from_slice::<ClientRequest>(&src) {
|
||||
Ok(msg) => {
|
||||
// Clear the buffer for the next message.
|
||||
src.clear();
|
||||
Ok(Some(msg))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for ClientCodec {
|
||||
type Item = ClientResponse;
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, msg: ClientResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let data = serde_cbor::to_vec(&msg).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
io::Error::new(io::ErrorKind::Other, "CBOR encode error")
|
||||
})?;
|
||||
debug!("Attempting to send response -> {:?} ...", data);
|
||||
dst.put(data.as_slice());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
fn new() -> Self {
|
||||
ClientCodec
|
||||
}
|
||||
}
|
||||
|
||||
fn rm_if_exist(p: &str) {
|
||||
let _ = std::fs::remove_file(p).map_err(|e| {
|
||||
error!("attempting to remove {:?} -> {:?}", p, e);
|
||||
()
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_client(
|
||||
sock: UnixStream,
|
||||
cachelayer: Arc<CacheLayer>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
debug!("Accepted connection");
|
||||
|
||||
let mut reqs = Framed::new(sock, ClientCodec::new());
|
||||
|
||||
while let Some(Ok(req)) = reqs.next().await {
|
||||
match req {
|
||||
ClientRequest::SshKey(account_id) => {
|
||||
let resp = match cachelayer.get_sshkeys(account_id.as_str()).await {
|
||||
Ok(r) => ClientResponse::SshKeys(r),
|
||||
Err(_) => {
|
||||
error!("unable to load keys, returning empty set.");
|
||||
ClientResponse::SshKeys(vec![])
|
||||
}
|
||||
};
|
||||
|
||||
reqs.send(resp).await?;
|
||||
reqs.flush().await?;
|
||||
debug!("flushed response!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect them
|
||||
debug!("Disconnecting client ...");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
|
||||
env_logger::init();
|
||||
rm_if_exist(DEFAULT_SOCK_PATH);
|
||||
|
||||
// setup
|
||||
let cb = KanidmClientBuilder::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/config")
|
||||
.expect("Failed to parse /etc/kanidm/config");
|
||||
|
||||
let cb = cb.connect_timeout(DEFAULT_CONN_TIMEOUT);
|
||||
|
||||
let rsclient = cb.build_async().expect("Failed to build async client");
|
||||
|
||||
let cachelayer = Arc::new(
|
||||
CacheLayer::new(
|
||||
DEFAULT_DB_PATH, // The sqlite db path
|
||||
DEFAULT_CACHE_TIMEOUT,
|
||||
rsclient,
|
||||
)
|
||||
.expect("Failed to build cache layer."),
|
||||
);
|
||||
|
||||
let mut listener = UnixListener::bind(DEFAULT_SOCK_PATH).unwrap();
|
||||
|
||||
let server = async move {
|
||||
let mut incoming = listener.incoming();
|
||||
while let Some(socket_res) = incoming.next().await {
|
||||
match socket_res {
|
||||
Ok(socket) => {
|
||||
let cachelayer_ref = cachelayer.clone();
|
||||
tokio::spawn(
|
||||
async move {
|
||||
if let Err(e) = handle_client(socket, cachelayer_ref.clone()).await {
|
||||
error!("an error occured; error = {:?}", e);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Accept error -> {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!("Server started ...");
|
||||
|
||||
server.await;
|
||||
}
|
||||
|
||||
// This is the actix version, but on MacOS there is an issue where it can't flush the socket properly :(
|
||||
|
||||
//=== A connected client session
|
||||
/*
|
||||
|
||||
struct ClientSession {
|
||||
framed: actix::io::FramedWrite<WriteHalf<UnixStream>, ClientCodec>,
|
||||
}
|
||||
|
||||
impl Actor for ClientSession {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
impl actix::io::WriteHandler<io::Error> for ClientSession {}
|
||||
|
||||
impl StreamHandler<Result<ClientRequest, io::Error>> for ClientSession {
|
||||
fn handle(&mut self, msg: Result<ClientRequest, io::Error>, ctx: &mut Self::Context) {
|
||||
debug!("Processing -> {:?}", msg);
|
||||
match msg {
|
||||
Ok(ClientRequest::SshKey(account_id)) => {
|
||||
self.framed.write(ClientResponse::SshKeys(vec![]));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Encountered an IO error, disconnecting session -> {:?}", e);
|
||||
ctx.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientSession {
|
||||
fn new(framed: actix::io::FramedWrite<WriteHalf<UnixStream>, ClientCodec>) -> Self {
|
||||
ClientSession { framed: framed }
|
||||
}
|
||||
}
|
||||
|
||||
//=== this is the accept server
|
||||
|
||||
struct AcceptServer;
|
||||
|
||||
impl Actor for AcceptServer {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
#[rtype(result = "()")]
|
||||
struct UdsConnect(pub UnixStream, pub SocketAddr);
|
||||
|
||||
impl Handler<UdsConnect> for AcceptServer {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UdsConnect, _: &mut Context<Self>) {
|
||||
debug!("Accepting new client ...");
|
||||
|
||||
// TODO: Clone the DB actor handle here.
|
||||
ClientSession::create(move |ctx| {
|
||||
let (r, w) = tokio::io::split(msg.0);
|
||||
ClientSession::add_stream(FramedRead::new(r, ClientCodec), ctx);
|
||||
ClientSession::new(actix::io::FramedWrite::new(w, ClientCodec, ctx))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() {
|
||||
// Setup logging
|
||||
::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
|
||||
env_logger::init();
|
||||
|
||||
rm_if_exist(DEFAULT_SOCK_PATH);
|
||||
let listener = Box::new(UnixListener::bind(DEFAULT_SOCK_PATH).expect("Failed to bind"));
|
||||
AcceptServer::create(|ctx| {
|
||||
ctx.add_message_stream(Box::leak(listener).incoming().map(|st| {
|
||||
let st = st.unwrap();
|
||||
let addr = st.peer_addr().unwrap();
|
||||
UdsConnect(st, addr)
|
||||
}));
|
||||
AcceptServer {}
|
||||
});
|
||||
println!("Running ...");
|
||||
tokio::signal::ctrl_c().await.unwrap();
|
||||
println!("Ctrl-C received, shutting down");
|
||||
System::current().stop();
|
||||
}
|
||||
*/
|
350
kanidm_unix_int/src/db.rs
Normal file
350
kanidm_unix_int/src/db.rs
Normal file
|
@ -0,0 +1,350 @@
|
|||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use rusqlite::NO_PARAMS;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub struct Db {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
lock: Mutex<()>,
|
||||
}
|
||||
|
||||
pub struct DbTxn<'a> {
|
||||
_guard: MutexGuard<'a, ()>,
|
||||
committed: bool,
|
||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub fn new(path: &str) -> Result<Self, ()> {
|
||||
let manager = SqliteConnectionManager::file(path);
|
||||
// We only build a single thread. If we need more than one, we'll
|
||||
// need to re-do this to account for path = "" for debug.
|
||||
let builder1 = Pool::builder().max_size(1);
|
||||
let pool = builder1.build(manager).map_err(|e| {
|
||||
error!("r2d2 error {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
Ok(Db {
|
||||
pool: pool,
|
||||
lock: Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write(&self) -> DbTxn {
|
||||
let guard = self.lock.try_lock().expect("Unable to lock");
|
||||
let conn = self
|
||||
.pool
|
||||
.get()
|
||||
.expect("Unable to get connection from pool!!!");
|
||||
DbTxn::new(conn, guard)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Db {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Db {{}}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DbTxn<'a> {
|
||||
pub fn new(
|
||||
conn: r2d2::PooledConnection<SqliteConnectionManager>,
|
||||
guard: MutexGuard<'a, ()>,
|
||||
) -> Self {
|
||||
// Start the transaction
|
||||
debug!("Starting db WR txn ...");
|
||||
conn.execute("BEGIN TRANSACTION", NO_PARAMS)
|
||||
.expect("Unable to begin transaction!");
|
||||
DbTxn {
|
||||
committed: false,
|
||||
conn,
|
||||
_guard: guard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn migrate(&self) -> Result<(), ()> {
|
||||
// Setup two tables - one for accounts, one for groups.
|
||||
// correctly index the columns.
|
||||
// Optional pw hash field
|
||||
self.conn
|
||||
.execute(
|
||||
"CREATE TABLE IF NOT EXISTS account_t (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
spn TEXT NOT NULL UNIQUE,
|
||||
gidnumber INTEGER NOT NULL UNIQUE,
|
||||
password BLOB,
|
||||
token BLOB NOT NULL,
|
||||
expiry NUMERIC NOT NULL
|
||||
)
|
||||
",
|
||||
NO_PARAMS,
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("sqlite account_t create error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
self.conn
|
||||
.execute(
|
||||
"CREATE TABLE IF NOT EXISTS group_t (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
spn TEXT NOT NULL UNIQUE,
|
||||
gidnumber INTEGER NOT NULL UNIQUE,
|
||||
token BLOB NOT NULL,
|
||||
expiry NUMERIC NOT NULL
|
||||
)
|
||||
",
|
||||
NO_PARAMS,
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("sqlite group_t create error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit(mut self) -> Result<(), ()> {
|
||||
debug!("Commiting BE txn");
|
||||
assert!(!self.committed);
|
||||
self.committed = true;
|
||||
|
||||
self.conn
|
||||
.execute("COMMIT TRANSACTION", NO_PARAMS)
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
debug!("sqlite commit failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_cache(&self) -> Result<(), ()> {
|
||||
self.conn
|
||||
.execute("DELETE FROM group_t", NO_PARAMS)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite delete group_t failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
self.conn
|
||||
.execute("DELETE FROM account_t", NO_PARAMS)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite delete group_t failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_account(&self, account_id: &str) -> Result<Option<(UnixUserToken, u64)>, ()> {
|
||||
let mut stmt = self.conn
|
||||
.prepare(
|
||||
"SELECT token, expiry FROM account_t WHERE uuid = :account_id OR name = :account_id OR spn = :account_id"
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("sqlite select prepare failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
// Makes tuple (token, expiry)
|
||||
let data_iter = stmt
|
||||
.query_map(&[account_id], |row| Ok((row.get(0)?, row.get(1)?)))
|
||||
.map_err(|e| {
|
||||
error!("sqlite query_map failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
let data: Result<Vec<(Vec<u8>, i64)>, _> = data_iter
|
||||
.map(|v| {
|
||||
v.map_err(|e| {
|
||||
error!("sqlite map failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let data = data?;
|
||||
|
||||
// Assert only one result?
|
||||
if data.len() >= 2 {
|
||||
error!("invalid db state, multiple entries matched query?");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let r: Result<Option<(_, _)>, ()> = data
|
||||
.first()
|
||||
.map(|(token, expiry)| {
|
||||
// token convert with cbor.
|
||||
let t = serde_cbor::from_slice(token.as_slice()).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
let e = u64::try_from(*expiry).map_err(|e| {
|
||||
error!("u64 convert error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
Ok((t, e))
|
||||
})
|
||||
.transpose();
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
pub fn update_account(&self, account: &UnixUserToken, expire: u64) -> Result<(), ()> {
|
||||
let data = serde_cbor::to_vec(account).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
let expire = i64::try_from(expire).map_err(|e| {
|
||||
error!("i64 convert error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
let mut stmt = self.conn
|
||||
.prepare("INSERT OR REPLACE INTO account_t (uuid, name, spn, gidnumber, token, expiry) VALUES (:uuid, :name, :spn, :gidnumber, :token, :expiry)")
|
||||
.map_err(|e| {
|
||||
error!("sqlite prepare error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
stmt.execute_named(&[
|
||||
(":uuid", &account.uuid),
|
||||
(":name", &account.name),
|
||||
(":spn", &account.spn),
|
||||
(":gidnumber", &account.gidnumber),
|
||||
(":token", &data),
|
||||
(":expiry", &expire),
|
||||
])
|
||||
.map(|r| {
|
||||
debug!("insert -> {:?}", r);
|
||||
()
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("sqlite execute_named error -> {:?}", e);
|
||||
()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Debug for DbTxn<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "DbTxn {{}}")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for DbTxn<'a> {
|
||||
// Abort
|
||||
fn drop(self: &mut Self) {
|
||||
if !self.committed {
|
||||
debug!("Aborting BE WR txn");
|
||||
self.conn
|
||||
.execute("ROLLBACK TRANSACTION", NO_PARAMS)
|
||||
.expect("Unable to rollback transaction! Can not proceed!!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Db;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_basic() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = db.write();
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
let mut ut1 = UnixUserToken {
|
||||
name: "testuser".to_string(),
|
||||
spn: "testuser@example.com".to_string(),
|
||||
displayname: "Test User".to_string(),
|
||||
gidnumber: 2000,
|
||||
uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(),
|
||||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
};
|
||||
|
||||
// test finding no account
|
||||
let r1 = dbtxn.get_account("testuser").unwrap();
|
||||
assert!(r1.is_none());
|
||||
let r2 = dbtxn.get_account("testuser@example.com").unwrap();
|
||||
assert!(r2.is_none());
|
||||
let r3 = dbtxn
|
||||
.get_account("0302b99c-f0f6-41ab-9492-852692b0fd16")
|
||||
.unwrap();
|
||||
assert!(r3.is_none());
|
||||
/*
|
||||
let r4 = dbtxn.get_account("2000").unwrap();
|
||||
assert!(r4.is_none());
|
||||
*/
|
||||
|
||||
// test adding an account
|
||||
dbtxn.update_account(&ut1, 0).unwrap();
|
||||
|
||||
// test we can get it.
|
||||
let r1 = dbtxn.get_account("testuser").unwrap();
|
||||
assert!(r1.is_some());
|
||||
let r2 = dbtxn.get_account("testuser@example.com").unwrap();
|
||||
assert!(r2.is_some());
|
||||
let r3 = dbtxn
|
||||
.get_account("0302b99c-f0f6-41ab-9492-852692b0fd16")
|
||||
.unwrap();
|
||||
assert!(r3.is_some());
|
||||
|
||||
// test adding an account that was renamed
|
||||
ut1.name = "testuser2".to_string();
|
||||
ut1.spn = "testuser2@example.com".to_string();
|
||||
dbtxn.update_account(&ut1, 0).unwrap();
|
||||
|
||||
// get the account
|
||||
let r1 = dbtxn.get_account("testuser").unwrap();
|
||||
assert!(r1.is_none());
|
||||
let r2 = dbtxn.get_account("testuser@example.com").unwrap();
|
||||
assert!(r2.is_none());
|
||||
let r1 = dbtxn.get_account("testuser2").unwrap();
|
||||
assert!(r1.is_some());
|
||||
let r2 = dbtxn.get_account("testuser2@example.com").unwrap();
|
||||
assert!(r2.is_some());
|
||||
let r3 = dbtxn
|
||||
.get_account("0302b99c-f0f6-41ab-9492-852692b0fd16")
|
||||
.unwrap();
|
||||
assert!(r3.is_some());
|
||||
|
||||
// Clear cache
|
||||
assert!(dbtxn.clear_cache().is_ok());
|
||||
|
||||
// should be nothing
|
||||
let r1 = dbtxn.get_account("testuser").unwrap();
|
||||
assert!(r1.is_none());
|
||||
let r2 = dbtxn.get_account("testuser@example.com").unwrap();
|
||||
assert!(r2.is_none());
|
||||
let r3 = dbtxn
|
||||
.get_account("0302b99c-f0f6-41ab-9492-852692b0fd16")
|
||||
.unwrap();
|
||||
assert!(r3.is_none());
|
||||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_group_basic() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let db = Db::new("").expect("failed to create.");
|
||||
let dbtxn = db.write();
|
||||
assert!(dbtxn.migrate().is_ok());
|
||||
|
||||
// test finding no account
|
||||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
// unimplemented!();
|
||||
}
|
||||
}
|
12
kanidm_unix_int/src/lib.rs
Normal file
12
kanidm_unix_int/src/lib.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
// #![deny(warnings)]
|
||||
#![warn(unused_extern_crates)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod cache;
|
||||
pub mod constants;
|
||||
pub(crate) mod db;
|
||||
pub mod unix_proto;
|
0
kanidm_unix_int/src/nsswitch.rs
Normal file
0
kanidm_unix_int/src/nsswitch.rs
Normal file
0
kanidm_unix_int/src/pam.rs
Normal file
0
kanidm_unix_int/src/pam.rs
Normal file
114
kanidm_unix_int/src/ssh_authorizedkeys.rs
Normal file
114
kanidm_unix_int/src/ssh_authorizedkeys.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use log::debug;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use futures::executor::block_on;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use std::error::Error;
|
||||
use std::io::Error as IoError;
|
||||
use std::io::ErrorKind;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio_util::codec::Framed;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
struct ClientCodec;
|
||||
|
||||
impl Decoder for ClientCodec {
|
||||
type Item = ClientResponse;
|
||||
type Error = IoError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
match serde_cbor::from_slice::<ClientResponse>(&src) {
|
||||
Ok(msg) => {
|
||||
// Clear the buffer for the next message.
|
||||
src.clear();
|
||||
Ok(Some(msg))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for ClientCodec {
|
||||
type Item = ClientRequest;
|
||||
type Error = IoError;
|
||||
|
||||
fn encode(&mut self, msg: ClientRequest, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let data = serde_cbor::to_vec(&msg).map_err(|e| {
|
||||
error!("socket encoding error -> {:?}", e);
|
||||
IoError::new(ErrorKind::Other, "CBOR encode error")
|
||||
})?;
|
||||
debug!("Attempting to send request -> {:?} ...", data);
|
||||
dst.put(data.as_slice());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
fn new() -> Self {
|
||||
ClientCodec
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct ClientOpt {
|
||||
#[structopt(short = "d", long = "debug")]
|
||||
debug: bool,
|
||||
#[structopt()]
|
||||
account_id: String,
|
||||
}
|
||||
|
||||
async fn call_daemon(path: &str, req: ClientRequest) -> Result<ClientResponse, Box<dyn Error>> {
|
||||
let stream = UnixStream::connect(path).await?;
|
||||
|
||||
let mut reqs = Framed::new(stream, ClientCodec::new());
|
||||
|
||||
reqs.send(req).await?;
|
||||
reqs.flush().await?;
|
||||
|
||||
match reqs.next().await {
|
||||
Some(Ok(res)) => {
|
||||
debug!("Response -> {:?}", res);
|
||||
Ok(res)
|
||||
}
|
||||
_ => {
|
||||
error!("Error");
|
||||
Err(Box::new(IoError::new(ErrorKind::Other, "oh no!")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let opt = ClientOpt::from_args();
|
||||
if opt.debug {
|
||||
::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
|
||||
} else {
|
||||
::std::env::set_var("RUST_LOG", "kanidm=info,kanidm_client=info");
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
debug!("Starting authorized keys tool ...");
|
||||
let req = ClientRequest::SshKey(opt.account_id.clone());
|
||||
|
||||
match block_on(call_daemon(DEFAULT_SOCK_PATH, req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::SshKeys(sk) => sk.iter().for_each(|k| {
|
||||
println!("{}", k);
|
||||
}),
|
||||
_ => {
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
9
kanidm_unix_int/src/unix_proto.rs
Normal file
9
kanidm_unix_int/src/unix_proto.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ClientRequest {
|
||||
SshKey(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum ClientResponse {
|
||||
SshKeys(Vec<String>),
|
||||
}
|
139
kanidm_unix_int/tests/cache_layer_test.rs
Normal file
139
kanidm_unix_int/tests/cache_layer_test.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use actix::prelude::*;
|
||||
use kanidm::config::{Configuration, IntegrationTestConfig};
|
||||
use kanidm::core::create_server_core;
|
||||
|
||||
use kanidm_unix_common::cache::CacheLayer;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use kanidm_client::{KanidmClient, KanidmClientBuilder};
|
||||
|
||||
static PORT_ALLOC: AtomicUsize = AtomicUsize::new(18080);
|
||||
static ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
||||
|
||||
fn run_test(fix_fn: fn(KanidmClient) -> (), test_fn: fn(CacheLayer) -> ()) {
|
||||
// ::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug");
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let port = PORT_ALLOC.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let int_config = Box::new(IntegrationTestConfig {
|
||||
admin_password: ADMIN_TEST_PASSWORD.to_string(),
|
||||
});
|
||||
|
||||
let mut config = Configuration::new();
|
||||
config.address = format!("127.0.0.1:{}", port);
|
||||
config.secure_cookies = false;
|
||||
config.integration_test_config = Some(int_config);
|
||||
// Setup the config ...
|
||||
|
||||
thread::spawn(move || {
|
||||
// Spawn a thread for the test runner, this should have a unique
|
||||
// port....
|
||||
System::run(move || {
|
||||
create_server_core(config);
|
||||
let _ = tx.send(System::current());
|
||||
});
|
||||
});
|
||||
let sys = rx.recv().unwrap();
|
||||
System::set_current(sys.clone());
|
||||
|
||||
// Setup the client, and the address we selected.
|
||||
let addr = format!("http://127.0.0.1:{}", port);
|
||||
|
||||
// Run fixtures
|
||||
let rsclient = KanidmClientBuilder::new()
|
||||
.address(addr.clone())
|
||||
.build()
|
||||
.expect("Failed to build sync client");
|
||||
fix_fn(rsclient);
|
||||
|
||||
let rsclient = KanidmClientBuilder::new()
|
||||
.address(addr)
|
||||
.build_async()
|
||||
.expect("Failed to build client");
|
||||
|
||||
let cachelayer = CacheLayer::new(
|
||||
"", // The sqlite db path, this is in memory.
|
||||
300, rsclient,
|
||||
)
|
||||
.expect("Failed to build cache layer.");
|
||||
|
||||
test_fn(cachelayer);
|
||||
|
||||
// We DO NOT need teardown, as sqlite is in mem
|
||||
// let the tables hit the floor
|
||||
sys.stop();
|
||||
}
|
||||
|
||||
fn test_fixture(rsclient: KanidmClient) -> () {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", vec!["admin"])
|
||||
.unwrap();
|
||||
|
||||
// Create a new account
|
||||
rsclient
|
||||
.idm_account_create("testaccount1", "Posix Demo Account")
|
||||
.unwrap();
|
||||
|
||||
// Extend the account with posix attrs.
|
||||
rsclient
|
||||
.idm_account_unix_extend("testaccount1", Some(20000), None)
|
||||
.unwrap();
|
||||
// Assign an ssh public key.
|
||||
rsclient
|
||||
.idm_account_post_ssh_pubkey("testaccount1", "tk",
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst")
|
||||
.unwrap();
|
||||
|
||||
// Setup a group
|
||||
rsclient.idm_group_create("testgroup1").unwrap();
|
||||
rsclient
|
||||
.idm_group_add_members("testgroup1", vec!["testaccount1"])
|
||||
.unwrap();
|
||||
rsclient
|
||||
.idm_group_unix_extend("testgroup1", Some(20001))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_sshkey() {
|
||||
run_test(test_fixture, |cachelayer| {
|
||||
let mut rt = Runtime::new().expect("Failed to start tokio");
|
||||
let fut = async move {
|
||||
// Force offline. Show we have no keys.
|
||||
cachelayer.mark_offline().await;
|
||||
|
||||
let sk = cachelayer
|
||||
.get_sshkeys("testaccount1")
|
||||
.await
|
||||
.expect("Failed to get from cache.");
|
||||
assert!(sk.len() == 0);
|
||||
|
||||
// Bring ourselves online.
|
||||
cachelayer.attempt_online().await;
|
||||
assert!(cachelayer.test_connection().await);
|
||||
|
||||
let sk = cachelayer
|
||||
.get_sshkeys("testaccount1")
|
||||
.await
|
||||
.expect("Failed to get from cache.");
|
||||
assert!(sk.len() == 1);
|
||||
|
||||
// Go offline, and get from cache.
|
||||
cachelayer.mark_offline().await;
|
||||
let sk = cachelayer
|
||||
.get_sshkeys("testaccount1")
|
||||
.await
|
||||
.expect("Failed to get from cache.");
|
||||
assert!(sk.len() == 1);
|
||||
};
|
||||
rt.block_on(fut);
|
||||
})
|
||||
}
|
|
@ -4,7 +4,7 @@ use crate::audit::AuditScope;
|
|||
|
||||
use crate::async_log::EventLog;
|
||||
use crate::event::{AuthEvent, SearchEvent, SearchResult, WhoamiResult};
|
||||
use crate::idm::event::RadiusAuthTokenEvent;
|
||||
use crate::idm::event::{RadiusAuthTokenEvent, UnixGroupTokenEvent, UnixUserTokenEvent};
|
||||
use crate::value::PartialValue;
|
||||
use kanidm_proto::v1::{OperationError, RadiusAuthToken};
|
||||
|
||||
|
@ -14,7 +14,8 @@ use crate::server::{QueryServer, QueryServerTransaction};
|
|||
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::{
|
||||
AuthRequest, AuthResponse, SearchRequest, SearchResponse, UserAuthToken, WhoamiResponse,
|
||||
AuthRequest, AuthResponse, SearchRequest, SearchResponse, UnixGroupToken, UnixUserToken,
|
||||
UserAuthToken, WhoamiResponse,
|
||||
};
|
||||
|
||||
use actix::prelude::*;
|
||||
|
@ -101,6 +102,24 @@ impl Message for InternalRadiusTokenReadMessage {
|
|||
type Result = Result<RadiusAuthToken, OperationError>;
|
||||
}
|
||||
|
||||
pub struct InternalUnixUserTokenReadMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
}
|
||||
|
||||
impl Message for InternalUnixUserTokenReadMessage {
|
||||
type Result = Result<UnixUserToken, OperationError>;
|
||||
}
|
||||
|
||||
pub struct InternalUnixGroupTokenReadMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
}
|
||||
|
||||
impl Message for InternalUnixGroupTokenReadMessage {
|
||||
type Result = Result<UnixGroupToken, OperationError>;
|
||||
}
|
||||
|
||||
pub struct InternalSshKeyReadMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
|
@ -422,6 +441,96 @@ impl Handler<InternalRadiusTokenReadMessage> for QueryServerReadV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<InternalUnixUserTokenReadMessage> for QueryServerReadV1 {
|
||||
type Result = Result<UnixUserToken, OperationError>;
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: InternalUnixUserTokenReadMessage,
|
||||
_: &mut Self::Context,
|
||||
) -> Self::Result {
|
||||
let mut audit = AuditScope::new("internal_unix_token_read_message");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let idm_read = self.idms.proxy_read();
|
||||
|
||||
let target_uuid = Uuid::parse_str(msg.uuid_or_name.as_str()).or_else(|_| {
|
||||
idm_read
|
||||
.qs_read
|
||||
.posixid_to_uuid(&mut audit, msg.uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
audit_log!(&mut audit, "Error resolving as gidnumber continuing ...");
|
||||
e
|
||||
})
|
||||
})?;
|
||||
|
||||
// Make an event from the request
|
||||
let rate = match UnixUserTokenEvent::from_parts(
|
||||
&mut audit,
|
||||
&idm_read.qs_read,
|
||||
msg.uat,
|
||||
target_uuid,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin search: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
audit_log!(audit, "Begin event {:?}", rate);
|
||||
|
||||
idm_read.get_unixusertoken(&mut audit, &rate)
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<InternalUnixGroupTokenReadMessage> for QueryServerReadV1 {
|
||||
type Result = Result<UnixGroupToken, OperationError>;
|
||||
|
||||
fn handle(
|
||||
&mut self,
|
||||
msg: InternalUnixGroupTokenReadMessage,
|
||||
_: &mut Self::Context,
|
||||
) -> Self::Result {
|
||||
let mut audit = AuditScope::new("internal_unixgroup_token_read_message");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let idm_read = self.idms.proxy_read();
|
||||
|
||||
let target_uuid = Uuid::parse_str(msg.uuid_or_name.as_str()).or_else(|_| {
|
||||
idm_read
|
||||
.qs_read
|
||||
.posixid_to_uuid(&mut audit, msg.uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
audit_log!(&mut audit, "Error resolving as gidnumber continuing ...");
|
||||
e
|
||||
})
|
||||
})?;
|
||||
|
||||
// Make an event from the request
|
||||
let rate = match UnixGroupTokenEvent::from_parts(
|
||||
&mut audit,
|
||||
&idm_read.qs_read,
|
||||
msg.uat,
|
||||
target_uuid,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin search: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
audit_log!(audit, "Begin event {:?}", rate);
|
||||
|
||||
idm_read.get_unixgrouptoken(&mut audit, &rate)
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<InternalSshKeyReadMessage> for QueryServerReadV1 {
|
||||
type Result = Result<Vec<String>, OperationError>;
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::event::{
|
|||
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
||||
};
|
||||
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent};
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
use crate::value::Value;
|
||||
use crate::modify::{Modify, ModifyInvalid, ModifyList};
|
||||
use crate::value::{PartialValue, Value};
|
||||
use kanidm_proto::v1::OperationError;
|
||||
|
||||
use crate::filter::{Filter, FilterInvalid};
|
||||
|
@ -18,8 +18,8 @@ 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::{
|
||||
CreateRequest, DeleteRequest, ModifyRequest, OperationResponse, SetAuthCredential,
|
||||
SingleStringRequest, UserAuthToken,
|
||||
AccountUnixExtend, CreateRequest, DeleteRequest, GroupUnixExtend, ModifyRequest,
|
||||
OperationResponse, SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||
};
|
||||
|
||||
use actix::prelude::*;
|
||||
|
@ -104,6 +104,50 @@ impl Message for IdmAccountSetPasswordMessage {
|
|||
type Result = Result<OperationResponse, OperationError>;
|
||||
}
|
||||
|
||||
pub struct IdmAccountUnixExtendMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
pub gidnumber: Option<u32>,
|
||||
pub shell: Option<String>,
|
||||
}
|
||||
|
||||
impl IdmAccountUnixExtendMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, uuid_or_name: String, ux: AccountUnixExtend) -> Self {
|
||||
let AccountUnixExtend { gidnumber, shell } = ux;
|
||||
IdmAccountUnixExtendMessage {
|
||||
uat,
|
||||
uuid_or_name,
|
||||
gidnumber,
|
||||
shell,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for IdmAccountUnixExtendMessage {
|
||||
type Result = Result<(), OperationError>;
|
||||
}
|
||||
|
||||
pub struct IdmGroupUnixExtendMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
pub gidnumber: Option<u32>,
|
||||
}
|
||||
|
||||
impl IdmGroupUnixExtendMessage {
|
||||
pub fn new(uat: Option<UserAuthToken>, uuid_or_name: String, gx: GroupUnixExtend) -> Self {
|
||||
let GroupUnixExtend { gidnumber } = gx;
|
||||
IdmGroupUnixExtendMessage {
|
||||
uat,
|
||||
uuid_or_name,
|
||||
gidnumber,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for IdmGroupUnixExtendMessage {
|
||||
type Result = Result<(), OperationError>;
|
||||
}
|
||||
|
||||
pub struct InternalCredentialSetMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
|
@ -751,6 +795,80 @@ impl Handler<InternalSshKeyCreateMessage> for QueryServerWriteV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<IdmAccountUnixExtendMessage> for QueryServerWriteV1 {
|
||||
type Result = Result<(), OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: IdmAccountUnixExtendMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("idm_account_unix_extend");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let IdmAccountUnixExtendMessage {
|
||||
uat,
|
||||
uuid_or_name,
|
||||
gidnumber,
|
||||
shell,
|
||||
} = msg;
|
||||
|
||||
// The filter_map here means we only create the mods if the gidnumber or shell are set
|
||||
// in the actual request.
|
||||
let mods: Vec<_> = vec![
|
||||
Some(Modify::Present(
|
||||
"class".to_string(),
|
||||
Value::new_class("posixaccount"),
|
||||
)),
|
||||
gidnumber.map(|n| Modify::Present("gidnumber".to_string(), Value::new_uint32(n))),
|
||||
shell.map(|s| Modify::Present("loginshell".to_string(), Value::new_iutf8(s))),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|v| v)
|
||||
.collect();
|
||||
|
||||
let ml = ModifyList::new_list(mods);
|
||||
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
|
||||
|
||||
self.modify_from_internal_parts(&mut audit, uat, uuid_or_name, ml, filter)
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<IdmGroupUnixExtendMessage> for QueryServerWriteV1 {
|
||||
type Result = Result<(), OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: IdmGroupUnixExtendMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("idm_group_unix_extend");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let IdmGroupUnixExtendMessage {
|
||||
uat,
|
||||
uuid_or_name,
|
||||
gidnumber,
|
||||
} = msg;
|
||||
|
||||
// The filter_map here means we only create the mods if the gidnumber or shell are set
|
||||
// in the actual request.
|
||||
let mods: Vec<_> = vec![
|
||||
Some(Modify::Present(
|
||||
"class".to_string(),
|
||||
Value::new_class("posixgroup"),
|
||||
)),
|
||||
gidnumber.map(|n| Modify::Present("gidnumber".to_string(), Value::new_uint32(n))),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|v| v)
|
||||
.collect();
|
||||
|
||||
let ml = ModifyList::new_list(mods);
|
||||
|
||||
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
|
||||
|
||||
self.modify_from_internal_parts(&mut audit, uat, uuid_or_name, ml, filter)
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
// These below are internal only types.
|
||||
|
||||
impl Handler<PurgeTombstoneEvent> for QueryServerWriteV1 {
|
||||
|
|
|
@ -39,6 +39,8 @@ pub static _UUID_IDM_ADMIN_V1: &str = "00000000-0000-0000-0000-000000000018";
|
|||
pub static _UUID_SYSTEM_ADMINS: &str = "00000000-0000-0000-0000-000000000019";
|
||||
// TODO
|
||||
pub static UUID_DOMAIN_ADMINS: &str = "00000000-0000-0000-0000-000000000020";
|
||||
pub static _UUID_IDM_ACCOUNT_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000021";
|
||||
pub static _UUID_IDM_GROUP_UNIX_EXTEND_PRIV: &str = "00000000-0000-0000-0000-000000000022";
|
||||
//
|
||||
pub static _UUID_IDM_HIGH_PRIVILEGE: &str = "00000000-0000-0000-0000-000000001000";
|
||||
|
||||
|
@ -106,6 +108,7 @@ pub static UUID_SCHEMA_CLASS_POSIXACCOUNT: &str = "00000000-0000-0000-0000-ffff0
|
|||
pub static UUID_SCHEMA_CLASS_POSIXGROUP: &str = "00000000-0000-0000-0000-ffff00000058";
|
||||
pub static UUID_SCHEMA_ATTR_BADLIST_PASSWORD: &str = "00000000-0000-0000-0000-ffff00000059";
|
||||
pub static UUID_SCHEMA_CLASS_SYSTEM_CONFIG: &str = "00000000-0000-0000-0000-ffff00000060";
|
||||
pub static UUID_SCHEMA_ATTR_LOGINSHELL: &str = "00000000-0000-0000-0000-ffff00000061";
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for fucking up these allocations.
|
||||
|
@ -142,6 +145,8 @@ pub static _UUID_IDM_ACP_HP_GROUP_MANAGE_PRIV_V1: &str = "00000000-0000-0000-000
|
|||
pub static UUID_IDM_ACP_DOMAIN_ADMIN_PRIV_V1: &str = "00000000-0000-0000-0000-ffffff000026";
|
||||
pub static STR_UUID_SYSTEM_CONFIG: &str = "00000000-0000-0000-0000-ffffff000027";
|
||||
pub static UUID_IDM_ACP_SYSTEM_CONFIG_PRIV_V1: &str = "00000000-0000-0000-0000-ffffff000028";
|
||||
pub static _UUID_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = "00000000-0000-0000-0000-ffffff000029";
|
||||
pub static _UUID_IDM_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str = "00000000-0000-0000-0000-ffffff000030";
|
||||
|
||||
// End of system ranges
|
||||
pub static STR_UUID_DOES_NOT_EXIST: &str = "00000000-0000-0000-0000-fffffffffffe";
|
||||
|
@ -267,6 +272,17 @@ pub static JSON_IDM_GROUP_WRITE_PRIV_V1: &str = r#"{
|
|||
]
|
||||
}
|
||||
}"#;
|
||||
pub static JSON_IDM_GROUP_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_group_unix_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000022"],
|
||||
"description": ["Builtin IDM Group for granting unix group extension permissions."],
|
||||
"member": [
|
||||
"00000000-0000-0000-0000-000000000001"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
// * account read manager
|
||||
pub static JSON_IDM_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
|
@ -300,6 +316,15 @@ pub static JSON_IDM_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"member": ["00000000-0000-0000-0000-000000000014"]
|
||||
}
|
||||
}"#;
|
||||
pub static JSON_IDM_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": ["group", "object"],
|
||||
"name": ["idm_account_unix_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-000000000021"],
|
||||
"description": ["Builtin IDM Group for granting account unix extend permissions."],
|
||||
"member": ["00000000-0000-0000-0000-000000000001"]
|
||||
}
|
||||
}"#;
|
||||
// * RADIUS servers
|
||||
pub static JSON_IDM_RADIUS_SERVERS_V1: &str = r#"{
|
||||
"attrs": {
|
||||
|
@ -545,8 +570,9 @@ pub static JSON_IDM_SELF_ACP_READ_V1: &str = r#"{
|
|||
"legalname",
|
||||
"class",
|
||||
"memberof",
|
||||
"member",
|
||||
"radius_secret",
|
||||
"gidnumber",
|
||||
"loginshell",
|
||||
"uuid"
|
||||
]
|
||||
}
|
||||
|
@ -595,6 +621,7 @@ pub static JSON_IDM_ALL_ACP_READ_V1: &str = r#"{
|
|||
"member",
|
||||
"uuid",
|
||||
"gidnumber",
|
||||
"loginshell",
|
||||
"ssh_publickey"
|
||||
]
|
||||
}
|
||||
|
@ -747,11 +774,10 @@ pub static JSON_IDM_ACP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
|
|||
"displayname",
|
||||
"description",
|
||||
"primary_credential",
|
||||
"ssh_publickey",
|
||||
"gidnumber"
|
||||
"ssh_publickey"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "account", "posixaccount"
|
||||
"object", "account"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -1122,11 +1148,10 @@ pub static JSON_IDM_ACP_GROUP_MANAGE_PRIV_V1: &str = r#"{
|
|||
"class",
|
||||
"name",
|
||||
"description",
|
||||
"gidnumber",
|
||||
"member"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "group", "posixgroup"
|
||||
"object", "group"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -1257,6 +1282,67 @@ pub static JSON_IDM_ACP_SYSTEM_CONFIG_PRIV_V1: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
// 29 account unix extend
|
||||
pub static JSON_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_search",
|
||||
"access_control_profile",
|
||||
"access_control_modify"
|
||||
],
|
||||
"name": ["idm_acp_account_unix_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000029"],
|
||||
"description": ["Builtin IDM Control for managing accounts."],
|
||||
"acp_receiver": [
|
||||
"{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000021\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"And\": [{\"Eq\": [\"class\",\"account\"]}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "description", "gidnumber", "loginshell"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "loginshell", "gidnumber"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "loginshell", "gidnumber"
|
||||
],
|
||||
"acp_modify_class": ["posixaccount"]
|
||||
}
|
||||
}"#;
|
||||
// 30 group unix extend
|
||||
pub static JSON_IDM_ACP_GROUP_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"access_control_profile",
|
||||
"access_control_search",
|
||||
"access_control_modify"
|
||||
],
|
||||
"name": ["idm_acp_group_unix_extend_priv"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000030"],
|
||||
"description": ["Builtin IDM Control for managing and extending unix groups"],
|
||||
"acp_receiver": [
|
||||
"{\"Eq\":[\"memberof\",\"00000000-0000-0000-0000-000000000022\"]}"
|
||||
],
|
||||
"acp_targetscope": [
|
||||
"{\"And\": [{\"Eq\": [\"class\",\"group\"]}, {\"AndNot\": {\"Or\": [{\"Eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"Eq\": [\"class\", \"tombstone\"]}, {\"Eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "description", "member", "gidnumber"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "gidnumber"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "gidnumber"
|
||||
],
|
||||
"acp_modify_class": ["posixgroup"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// Anonymous should be the last opbject in the range here.
|
||||
pub static JSON_ANONYMOUS_V1: &str = r#"{
|
||||
"attrs": {
|
||||
|
@ -1568,7 +1654,9 @@ pub static JSON_SCHEMA_ATTR_GIDNUMBER: &str = r#"{
|
|||
"description": [
|
||||
"The groupid (uid) number of a group or account. This is the same value as the UID number on posix accounts for security reasons."
|
||||
],
|
||||
"index": [],
|
||||
"index": [
|
||||
"EQUALITY"
|
||||
],
|
||||
"unique": [
|
||||
"true"
|
||||
],
|
||||
|
@ -1616,6 +1704,35 @@ pub static JSON_SCHEMA_ATTR_BADLIST_PASSWORD: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub static JSON_SCHEMA_ATTR_LOGINSHELL: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A posix users unix login shell"
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"loginshell"
|
||||
],
|
||||
"syntax": [
|
||||
"UTF8STRING_INSENSITIVE"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000061"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub static JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
{
|
||||
"valid": {
|
||||
|
@ -1783,6 +1900,9 @@ pub static JSON_SCHEMA_CLASS_POSIXACCOUNT: &str = r#"
|
|||
"classname": [
|
||||
"posixaccount"
|
||||
],
|
||||
"systemmay": [
|
||||
"loginshell"
|
||||
],
|
||||
"systemmust": [
|
||||
"gidnumber"
|
||||
],
|
||||
|
|
|
@ -17,14 +17,15 @@ use crate::config::Configuration;
|
|||
use crate::actors::v1_read::QueryServerReadV1;
|
||||
use crate::actors::v1_read::{
|
||||
AuthMessage, InternalRadiusReadMessage, InternalRadiusTokenReadMessage, InternalSearchMessage,
|
||||
InternalSshKeyReadMessage, InternalSshKeyTagReadMessage, SearchMessage, WhoamiMessage,
|
||||
InternalSshKeyReadMessage, InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage,
|
||||
InternalUnixUserTokenReadMessage, SearchMessage, WhoamiMessage,
|
||||
};
|
||||
use crate::actors::v1_write::QueryServerWriteV1;
|
||||
use crate::actors::v1_write::{
|
||||
AppendAttributeMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage,
|
||||
InternalCredentialSetMessage, InternalDeleteMessage, InternalRegenerateRadiusMessage,
|
||||
InternalSshKeyCreateMessage, ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage,
|
||||
SetAttributeMessage,
|
||||
IdmAccountUnixExtendMessage, IdmGroupUnixExtendMessage, InternalCredentialSetMessage,
|
||||
InternalDeleteMessage, InternalRegenerateRadiusMessage, InternalSshKeyCreateMessage,
|
||||
ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage, SetAttributeMessage,
|
||||
};
|
||||
use crate::async_log;
|
||||
use crate::audit::AuditScope;
|
||||
|
@ -42,8 +43,8 @@ use crate::value::PartialValue;
|
|||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::{
|
||||
AuthRequest, AuthState, CreateRequest, DeleteRequest, ModifyRequest, SearchRequest,
|
||||
SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||
AccountUnixExtend, AuthRequest, AuthState, CreateRequest, DeleteRequest, GroupUnixExtend,
|
||||
ModifyRequest, SearchRequest, SetAuthCredential, SingleStringRequest, UserAuthToken,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
@ -889,6 +890,70 @@ fn account_get_id_radius_token(
|
|||
Box::new(res)
|
||||
}
|
||||
|
||||
fn account_post_id_unix(
|
||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let max_size = state.max_size;
|
||||
let uat = get_current_user(&req);
|
||||
let id = path.into_inner();
|
||||
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::<AccountUnixExtend>(&body);
|
||||
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
let m_obj = IdmAccountUnixExtendMessage::new(uat, id, obj);
|
||||
let res = state.qe_w.send(m_obj).from_err().and_then(|res| match res {
|
||||
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
||||
Err(e) => Ok(operation_error_to_response(e)),
|
||||
});
|
||||
|
||||
Box::new(res)
|
||||
}
|
||||
Err(e) => Box::new(future::err(error::ErrorBadRequest(format!(
|
||||
"Json Decode Failed: {:?}",
|
||||
e
|
||||
)))),
|
||||
} // end match
|
||||
},
|
||||
) // end and_then
|
||||
}
|
||||
|
||||
fn account_get_id_unix_token(
|
||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let uat = get_current_user(&req);
|
||||
let id = path.into_inner();
|
||||
|
||||
let obj = InternalUnixUserTokenReadMessage {
|
||||
uat,
|
||||
uuid_or_name: id,
|
||||
};
|
||||
|
||||
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||
Ok(event_result) => {
|
||||
// Only send back the first result, or None
|
||||
Ok(HttpResponse::Ok().json(event_result))
|
||||
}
|
||||
Err(e) => Ok(operation_error_to_response(e)),
|
||||
});
|
||||
|
||||
Box::new(res)
|
||||
}
|
||||
|
||||
fn group_get(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
|
@ -961,6 +1026,70 @@ fn group_id_delete(
|
|||
json_rest_event_delete_id(path, req, state, filter)
|
||||
}
|
||||
|
||||
fn group_post_id_unix(
|
||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let max_size = state.max_size;
|
||||
let uat = get_current_user(&req);
|
||||
let id = path.into_inner();
|
||||
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::<GroupUnixExtend>(&body);
|
||||
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
let m_obj = IdmGroupUnixExtendMessage::new(uat, id, obj);
|
||||
let res = state.qe_w.send(m_obj).from_err().and_then(|res| match res {
|
||||
Ok(event_result) => Ok(HttpResponse::Ok().json(event_result)),
|
||||
Err(e) => Ok(operation_error_to_response(e)),
|
||||
});
|
||||
|
||||
Box::new(res)
|
||||
}
|
||||
Err(e) => Box::new(future::err(error::ErrorBadRequest(format!(
|
||||
"Json Decode Failed: {:?}",
|
||||
e
|
||||
)))),
|
||||
} // end match
|
||||
},
|
||||
) // end and_then
|
||||
}
|
||||
|
||||
fn group_get_id_unix_token(
|
||||
(path, req, state): (Path<String>, HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
let uat = get_current_user(&req);
|
||||
let id = path.into_inner();
|
||||
|
||||
let obj = InternalUnixGroupTokenReadMessage {
|
||||
uat,
|
||||
uuid_or_name: id,
|
||||
};
|
||||
|
||||
let res = state.qe_r.send(obj).from_err().and_then(|res| match res {
|
||||
Ok(event_result) => {
|
||||
// Only send back the first result, or None
|
||||
Ok(HttpResponse::Ok().json(event_result))
|
||||
}
|
||||
Err(e) => Ok(operation_error_to_response(e)),
|
||||
});
|
||||
|
||||
Box::new(res)
|
||||
}
|
||||
|
||||
fn domain_get(
|
||||
(req, state): (HttpRequest<AppState>, State<AppState>),
|
||||
) -> impl Future<Item = HttpResponse, Error = Error> {
|
||||
|
@ -1737,6 +1866,15 @@ pub fn create_server_core(config: Configuration) {
|
|||
r.method(http::Method::GET)
|
||||
.with_async(account_get_id_radius_token)
|
||||
})
|
||||
// Get the accounts unix info token.
|
||||
.resource("/v1/account/{id}/_unix", |r| {
|
||||
r.method(http::Method::POST)
|
||||
.with_async(account_post_id_unix);
|
||||
})
|
||||
.resource("/v1/account/{id}/_unix/_token", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with_async(account_get_id_unix_token)
|
||||
})
|
||||
// People
|
||||
// Groups
|
||||
.resource("/v1/group", |r| {
|
||||
|
@ -1755,6 +1893,13 @@ pub fn create_server_core(config: Configuration) {
|
|||
r.method(http::Method::DELETE)
|
||||
.with_async(group_id_delete_attr);
|
||||
})
|
||||
.resource("/v1/group/{id}/_unix", |r| {
|
||||
r.method(http::Method::POST).with_async(group_post_id_unix);
|
||||
})
|
||||
.resource("/v1/group/{id}/_unix/_token", |r| {
|
||||
r.method(http::Method::GET)
|
||||
.with_async(group_get_id_unix_token)
|
||||
})
|
||||
// Claims
|
||||
// TBD
|
||||
// Domain
|
||||
|
|
|
@ -1170,6 +1170,13 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_ava_single_uint32(&self, attr: &str) -> Option<u32> {
|
||||
match self.get_ava_single(attr) {
|
||||
Some(a) => a.to_uint32(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ava_single_syntax(&self, attr: &str) -> Option<&SyntaxType> {
|
||||
match self.get_ava_single(attr) {
|
||||
Some(a) => a.to_syntaxtype(),
|
||||
|
|
|
@ -136,3 +136,55 @@ impl RadiusAuthTokenEvent {
|
|||
RadiusAuthTokenEvent { event: e, target }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnixUserTokenEvent {
|
||||
pub event: Event,
|
||||
pub target: Uuid,
|
||||
}
|
||||
|
||||
impl UnixUserTokenEvent {
|
||||
pub fn from_parts(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerReadTransaction,
|
||||
uat: Option<UserAuthToken>,
|
||||
target: Uuid,
|
||||
) -> Result<Self, OperationError> {
|
||||
let e = Event::from_ro_uat(audit, qs, uat)?;
|
||||
|
||||
Ok(UnixUserTokenEvent { event: e, target })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid) -> Self {
|
||||
let e = Event::from_internal();
|
||||
|
||||
UnixUserTokenEvent { event: e, target }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnixGroupTokenEvent {
|
||||
pub event: Event,
|
||||
pub target: Uuid,
|
||||
}
|
||||
|
||||
impl UnixGroupTokenEvent {
|
||||
pub fn from_parts(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerReadTransaction,
|
||||
uat: Option<UserAuthToken>,
|
||||
target: Uuid,
|
||||
) -> Result<Self, OperationError> {
|
||||
let e = Event::from_ro_uat(audit, qs, uat)?;
|
||||
|
||||
Ok(UnixGroupTokenEvent { event: e, target })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: Uuid) -> Self {
|
||||
let e = Event::from_internal();
|
||||
|
||||
UnixGroupTokenEvent { event: e, target }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,4 +8,5 @@ pub(crate) mod event;
|
|||
pub(crate) mod group;
|
||||
pub(crate) mod radius;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod unix;
|
||||
// mod identity;
|
||||
|
|
|
@ -6,8 +6,10 @@ use crate::idm::account::Account;
|
|||
use crate::idm::authsession::AuthSession;
|
||||
use crate::idm::event::{
|
||||
GeneratePasswordEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
UnixGroupTokenEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::idm::radius::RadiusAccount;
|
||||
use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||
use crate::server::QueryServerReadTransaction;
|
||||
use crate::server::{QueryServer, QueryServerTransaction, QueryServerWriteTransaction};
|
||||
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, SID};
|
||||
|
@ -16,6 +18,8 @@ use crate::value::PartialValue;
|
|||
use kanidm_proto::v1::AuthState;
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::RadiusAuthToken;
|
||||
use kanidm_proto::v1::UnixGroupToken;
|
||||
use kanidm_proto::v1::UnixUserToken;
|
||||
|
||||
use concread::collections::bptree::*;
|
||||
use std::time::Duration;
|
||||
|
@ -231,6 +235,39 @@ impl IdmServerProxyReadTransaction {
|
|||
|
||||
account.to_radiusauthtoken()
|
||||
}
|
||||
|
||||
pub fn get_unixusertoken(
|
||||
&self,
|
||||
au: &mut AuditScope,
|
||||
uute: &UnixUserTokenEvent,
|
||||
) -> Result<UnixUserToken, OperationError> {
|
||||
let account_entry = try_audit!(
|
||||
au,
|
||||
self.qs_read
|
||||
.impersonate_search_ext_uuid(au, &uute.target, &uute.event)
|
||||
);
|
||||
|
||||
let account = try_audit!(
|
||||
au,
|
||||
UnixUserAccount::try_from_entry_reduced(au, account_entry, &self.qs_read)
|
||||
);
|
||||
account.to_unixusertoken()
|
||||
}
|
||||
|
||||
pub fn get_unixgrouptoken(
|
||||
&self,
|
||||
au: &mut AuditScope,
|
||||
uute: &UnixGroupTokenEvent,
|
||||
) -> Result<UnixGroupToken, OperationError> {
|
||||
let account_entry = try_audit!(
|
||||
au,
|
||||
self.qs_read
|
||||
.impersonate_search_ext_uuid(au, &uute.target, &uute.event)
|
||||
);
|
||||
|
||||
let account = try_audit!(au, UnixGroup::try_from_entry_reduced(account_entry));
|
||||
account.to_unixgrouptoken()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
|
@ -452,9 +489,11 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
mod tests {
|
||||
use crate::constants::{AUTH_SESSION_TIMEOUT, UUID_ADMIN, UUID_ANONYMOUS};
|
||||
use crate::credential::Credential;
|
||||
use crate::event::{AuthEvent, AuthResult, ModifyEvent};
|
||||
use crate::entry::{Entry, EntryInvalid, EntryNew};
|
||||
use crate::event::{AuthEvent, AuthResult, CreateEvent, ModifyEvent};
|
||||
use crate::idm::event::{
|
||||
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
UnixGroupTokenEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
use crate::value::{PartialValue, Value};
|
||||
|
@ -823,4 +862,63 @@ mod tests {
|
|||
assert!(idms_prox_write.commit(au).is_ok());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_unixusertoken() {
|
||||
run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| {
|
||||
let mut idms_prox_write = idms.proxy_write();
|
||||
// Modify admin to have posixaccount
|
||||
let me_posix = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq("name", PartialValue::new_iutf8s("admin"))),
|
||||
ModifyList::new_list(vec![
|
||||
Modify::Present("class".to_string(), Value::new_class("posixaccount")),
|
||||
Modify::Present("gidnumber".to_string(), Value::new_uint32(2001)),
|
||||
]),
|
||||
)
|
||||
};
|
||||
assert!(idms_prox_write.qs_write.modify(au, &me_posix).is_ok());
|
||||
// Add a posix group that has the admin as a member.
|
||||
let e: Entry<EntryInvalid, EntryNew> = Entry::unsafe_from_entry_str(
|
||||
r#"{
|
||||
"attrs": {
|
||||
"class": ["object", "group", "posixgroup"],
|
||||
"name": ["testgroup"],
|
||||
"uuid": ["01609135-a1c4-43d5-966b-a28227644445"],
|
||||
"description": ["testgroup"],
|
||||
"member": ["00000000-0000-0000-0000-000000000000"]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let ce = CreateEvent::new_internal(vec![e.clone()]);
|
||||
|
||||
assert!(idms_prox_write.qs_write.create(au, &ce).is_ok());
|
||||
|
||||
idms_prox_write.commit(au).expect("failed to commit");
|
||||
|
||||
let idms_prox_read = idms.proxy_read();
|
||||
|
||||
let ugte = UnixGroupTokenEvent::new_internal(
|
||||
Uuid::parse_str("01609135-a1c4-43d5-966b-a28227644445")
|
||||
.expect("failed to parse uuid"),
|
||||
);
|
||||
let tok_g = idms_prox_read
|
||||
.get_unixgrouptoken(au, &ugte)
|
||||
.expect("Failed to generate unix group token");
|
||||
|
||||
assert!(tok_g.name == "testgroup");
|
||||
assert!(tok_g.spn == "testgroup@example.com");
|
||||
|
||||
let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN.clone());
|
||||
let tok_r = idms_prox_read
|
||||
.get_unixusertoken(au, &uute)
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
assert!(tok_r.spn == "admin@example.com");
|
||||
assert!(tok_r.groups.len() == 1);
|
||||
assert!(tok_r.groups[0].name == "testgroup");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
200
kanidmd/src/lib/idm/unix.rs
Normal file
200
kanidmd/src/lib/idm/unix.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::audit::AuditScope;
|
||||
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntryValid};
|
||||
use crate::server::{QueryServerReadTransaction, QueryServerTransaction};
|
||||
use crate::value::PartialValue;
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UnixUserAccount {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub displayname: String,
|
||||
pub gidnumber: u32,
|
||||
pub uuid: Uuid,
|
||||
pub shell: Option<String>,
|
||||
pub sshkeys: Vec<String>,
|
||||
pub groups: Vec<UnixGroup>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
||||
static ref PVCLASS_POSIXACCOUNT: PartialValue = PartialValue::new_class("posixaccount");
|
||||
static ref PVCLASS_GROUP: PartialValue = PartialValue::new_class("group");
|
||||
static ref PVCLASS_POSIXGROUP: PartialValue = PartialValue::new_class("posixgroup");
|
||||
}
|
||||
|
||||
impl UnixUserAccount {
|
||||
pub(crate) fn try_from_entry_reduced(
|
||||
au: &mut AuditScope,
|
||||
value: Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
if !value.attribute_value_pres("class", &PVCLASS_ACCOUNT) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: account".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !value.attribute_value_pres("class", &PVCLASS_POSIXACCOUNT) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: posixaccount".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let name = value.get_ava_single_string("name").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
|
||||
let spn = value
|
||||
.get_ava_single("spn")
|
||||
.map(|v| v.to_proto_string_clone())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string())
|
||||
})?;
|
||||
|
||||
let uuid = *value.get_uuid();
|
||||
|
||||
let displayname = value.get_ava_single_string("displayname").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: displayname".to_string())
|
||||
})?;
|
||||
|
||||
let gidnumber = value.get_ava_single_uint32("gidnumber").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: gidnumber".to_string())
|
||||
})?;
|
||||
|
||||
let shell = value.get_ava_single_string("loginshell");
|
||||
|
||||
let sshkeys = value.get_ava_ssh_pubkeys("ssh_publickey");
|
||||
|
||||
let groups = UnixGroup::try_from_account_entry_red_ro(au, &value, qs)?;
|
||||
|
||||
Ok(UnixUserAccount {
|
||||
name,
|
||||
spn,
|
||||
uuid,
|
||||
displayname,
|
||||
gidnumber,
|
||||
shell,
|
||||
sshkeys,
|
||||
groups,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixusertoken(&self) -> Result<UnixUserToken, OperationError> {
|
||||
let groups: Result<Vec<_>, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect();
|
||||
let groups = groups?;
|
||||
|
||||
Ok(UnixUserToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
displayname: self.name.clone(),
|
||||
gidnumber: self.gidnumber,
|
||||
uuid: self.uuid.to_hyphenated_ref().to_string(),
|
||||
shell: self.shell.clone(),
|
||||
groups: groups,
|
||||
sshkeys: self.sshkeys.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct UnixGroup {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
pub gidnumber: u32,
|
||||
pub uuid: Uuid,
|
||||
}
|
||||
|
||||
macro_rules! try_from_group_e {
|
||||
($value:expr) => {{
|
||||
if !$value.attribute_value_pres("class", &PVCLASS_GROUP) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: group".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !$value.attribute_value_pres("class", &PVCLASS_POSIXGROUP) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Missing class: posixgroup".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let name = $value.get_ava_single_string("name").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
|
||||
let spn = $value
|
||||
.get_ava_single("spn")
|
||||
.map(|v| v.to_proto_string_clone())
|
||||
.ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: spn".to_string())
|
||||
})?;
|
||||
|
||||
let uuid = *$value.get_uuid();
|
||||
|
||||
let gidnumber = $value.get_ava_single_uint32("gidnumber").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: gidnumber".to_string())
|
||||
})?;
|
||||
|
||||
Ok(UnixGroup {
|
||||
name,
|
||||
spn,
|
||||
gidnumber,
|
||||
uuid,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixGroup {
|
||||
pub fn try_from_account_entry_red_ro(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
match value.get_ava_reference_uuid("memberof") {
|
||||
Some(l) => {
|
||||
let f = filter!(f_and!([
|
||||
f_eq("class", PartialValue::new_class("posixgroup")),
|
||||
f_eq("class", PartialValue::new_class("group")),
|
||||
f_or(
|
||||
l.into_iter()
|
||||
.map(|u| f_eq("uuid", PartialValue::new_uuidr(u)))
|
||||
.collect()
|
||||
)
|
||||
]));
|
||||
let ges: Vec<_> = try_audit!(au, qs.internal_search(au, f));
|
||||
let groups: Result<Vec<_>, _> =
|
||||
ges.into_iter().map(UnixGroup::try_from_entry).collect();
|
||||
groups
|
||||
}
|
||||
None => {
|
||||
// No memberof, no groups!
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_entry_reduced(
|
||||
value: Entry<EntryReduced, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub fn try_from_entry(
|
||||
value: Entry<EntryValid, EntryCommitted>,
|
||||
) -> Result<Self, OperationError> {
|
||||
try_from_group_e!(value)
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixgrouptoken(&self) -> Result<UnixGroupToken, OperationError> {
|
||||
Ok(UnixGroupToken {
|
||||
name: self.name.clone(),
|
||||
spn: self.spn.clone(),
|
||||
uuid: self.uuid.to_hyphenated_ref().to_string(),
|
||||
gidnumber: self.gidnumber,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ use crate::event::{
|
|||
CreateEvent, DeleteEvent, Event, EventOrigin, ExistsEvent, ModifyEvent, ReviveRecycledEvent,
|
||||
SearchEvent,
|
||||
};
|
||||
use crate::filter::{Filter, FilterInvalid, FilterValid};
|
||||
use crate::filter::{f_eq, Filter, FilterInvalid, FilterValid};
|
||||
use crate::modify::{Modify, ModifyInvalid, ModifyList, ModifyValid};
|
||||
use crate::plugins::Plugins;
|
||||
use crate::schema::{
|
||||
|
@ -167,11 +167,10 @@ pub trait QueryServerTransaction {
|
|||
// index searches, completely bypassing id2entry.
|
||||
|
||||
// construct the filter
|
||||
// Internal search - DO NOT SEARCH TOMBSTONES AND RECYCLE
|
||||
let filt = filter!(f_eq("name", PartialValue::new_iutf8s(name)));
|
||||
audit_log!(audit, "name_to_uuid: name -> {:?}", name);
|
||||
|
||||
// Internal search - DO NOT SEARCH TOMBSTONES AND RECYCLE
|
||||
// TODO: Should we just search everything? It's a uuid to name ...
|
||||
let res = match self.internal_search(audit, filt) {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Err(e),
|
||||
|
@ -247,6 +246,43 @@ pub trait QueryServerTransaction {
|
|||
Ok(Some(name_res))
|
||||
}
|
||||
|
||||
fn posixid_to_uuid(&self, audit: &mut AuditScope, name: &str) -> Result<Uuid, OperationError> {
|
||||
let f_name = Some(f_eq("name", PartialValue::new_iutf8s(name)));
|
||||
|
||||
let f_spn = PartialValue::new_spn_s(name).map(|v| f_eq("spn", v));
|
||||
|
||||
let f_gidnumber = PartialValue::new_uint32_str(name).map(|v| f_eq("gidnumber", v));
|
||||
|
||||
let x = vec![f_name, f_spn, f_gidnumber];
|
||||
|
||||
let filt = filter!(f_or(x.into_iter().filter_map(|v| v).collect()));
|
||||
audit_log!(audit, "posixid_to_uuid: name -> {:?}", name);
|
||||
|
||||
let res = match self.internal_search(audit, filt) {
|
||||
Ok(e) => e,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
audit_log!(audit, "posixid_to_uuid: results -- {:?}", res);
|
||||
|
||||
if res.is_empty() {
|
||||
// If result len == 0, error no such result
|
||||
return Err(OperationError::NoMatchingEntries);
|
||||
} else if res.len() >= 2 {
|
||||
// if result len >= 2, error, invaid entry state.
|
||||
return Err(OperationError::InvalidDBState);
|
||||
}
|
||||
|
||||
// error should never be triggered due to the len checks above.
|
||||
let e = res.first().ok_or(OperationError::NoMatchingEntries)?;
|
||||
// Get the uuid from the entry. Again, check it exists, and only one.
|
||||
let uuid_res: Uuid = *e.get_uuid();
|
||||
|
||||
audit_log!(audit, "posixid_to_uuid: uuid <- {:?}", uuid_res);
|
||||
|
||||
Ok(uuid_res)
|
||||
}
|
||||
|
||||
// From internal, generate an exists event and dispatch
|
||||
fn internal_exists(
|
||||
&self,
|
||||
|
@ -1675,6 +1711,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_DOMAIN_SSID,
|
||||
JSON_SCHEMA_ATTR_GIDNUMBER,
|
||||
JSON_SCHEMA_ATTR_BADLIST_PASSWORD,
|
||||
JSON_SCHEMA_ATTR_LOGINSHELL,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||
|
@ -1752,8 +1789,10 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_PEOPLE_READ_PRIV_V1,
|
||||
JSON_IDM_GROUP_MANAGE_PRIV_V1,
|
||||
JSON_IDM_GROUP_WRITE_PRIV_V1,
|
||||
JSON_IDM_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACCOUNT_MANAGE_PRIV_V1,
|
||||
JSON_IDM_ACCOUNT_WRITE_PRIV_V1,
|
||||
JSON_IDM_ACCOUNT_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACCOUNT_READ_PRIV_V1,
|
||||
JSON_IDM_RADIUS_SERVERS_V1,
|
||||
// Write deps on read, so write must be added first.
|
||||
|
@ -1792,6 +1831,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_IDM_ACP_ACP_MANAGE_PRIV_V1,
|
||||
JSON_IDM_ACP_DOMAIN_ADMIN_PRIV_V1,
|
||||
JSON_IDM_ACP_SYSTEM_CONFIG_PRIV_V1,
|
||||
JSON_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1,
|
||||
JSON_IDM_ACP_GROUP_UNIX_EXTEND_PRIV_V1,
|
||||
];
|
||||
|
||||
let res: Result<(), _> = idm_entries
|
||||
|
|
|
@ -1108,6 +1108,13 @@ impl Value {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_uint32(&self) -> Option<u32> {
|
||||
match &self.pv {
|
||||
PartialValue::Uint32(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_partialvalue(&self) -> PartialValue {
|
||||
// Match on self to become a partialvalue.
|
||||
self.pv.clone()
|
||||
|
|
Loading…
Reference in a new issue