mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
20200218 pam (#189)
Add support for unix_password handling, and pam authentication for services.
This commit is contained in:
parent
b048115698
commit
5a9ad39d6b
328
Cargo.lock
generated
328
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,11 @@ members = [
|
|||
"kanidm_client",
|
||||
"kanidm_tools",
|
||||
"kanidm_unix_int",
|
||||
"kanidm_unix_int/nss_kanidm",
|
||||
"kanidm_unix_int/pam_kanidm"
|
||||
"kanidm_unix_int/nss_kanidm",
|
||||
"kanidm_unix_int/pam_kanidm",
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"kanidm_unix_int/pam_tester"
|
||||
]
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -9,3 +9,5 @@ vendor-prep:
|
|||
cargo vendor
|
||||
tar -czf vendor.tar.gz vendor
|
||||
|
||||
doc-local:
|
||||
cargo doc --document-private-items
|
||||
|
|
|
@ -18,8 +18,14 @@ You can check the daemon is running on your Linux system with
|
|||
|
||||
# systemctl status kanidm_unixd
|
||||
|
||||
This daemon uses configuration from /etc/kanidm/config. This is the covered in
|
||||
client_tools.
|
||||
This daemon uses connection configuration from /etc/kanidm/config. This is the covered in
|
||||
client_tools. You can also configure some details of the unixd daemon in /etc/kanidm/unixd.
|
||||
|
||||
pam_allowed_login_groups = ["posix_group"]
|
||||
|
||||
The `pam_allowed_login_groups` defines a set of posix groups where membership of any of these
|
||||
groups will be allowed to login via pam. All posix users and groups can be resolved by nss
|
||||
regardless of pam login status.
|
||||
|
||||
You can then check the communication status of the daemon as any user account.
|
||||
|
||||
|
@ -39,8 +45,8 @@ For more, see troubleshooting.
|
|||
|
||||
When the daemon is running you can add the nsswitch libraries to /etc/nsswitch.conf
|
||||
|
||||
passwd: kanidm compat
|
||||
group: kanidm compat
|
||||
passwd: compat kanidm
|
||||
group: compat kanidm
|
||||
|
||||
You can then test that a posix extended user is able to be resolved with:
|
||||
|
||||
|
@ -61,10 +67,77 @@ You can also do the same for groups.
|
|||
> shell open while making changes (ie root), or have access to single-user mode
|
||||
> at the machines console.
|
||||
|
||||
TBD
|
||||
PAM (Pluggable Authentication Modules) is how a unix like system allows users to authenticate
|
||||
and be authorised to start interactive sessions. This is configured through a stack of modules
|
||||
that are executed in order to evaluate the request. This is done through a series of steps
|
||||
where each module may request or reused authentication token information.
|
||||
|
||||
### Before you start
|
||||
|
||||
You *should* backup your /etc/pam.d directory from it's original state as you *may* change the
|
||||
pam config in a way that will cause you to be unable to authenticate to your machine.
|
||||
|
||||
cp -a /etc/pam.d /root/pam.d.backup
|
||||
|
||||
### SUSE
|
||||
|
||||
To configure PAM on suse you must module four files:
|
||||
|
||||
/etc/pam.d/common-account-pc
|
||||
/etc/pam.d/common-auth-pc
|
||||
/etc/pam.d/common-password-pc
|
||||
/etc/pam.d/common-session-pc
|
||||
|
||||
Each of these controls one of the four stages of pam. The content should look like:
|
||||
|
||||
# /etc/pam.d/common-account-pc
|
||||
account [default=1 ignore=ignore success=ok] pam_localuser.so
|
||||
account required pam_unix.so
|
||||
account required pam_kanidm.so ignore_unknown_user
|
||||
|
||||
# /etc/pam.d/common-auth-pc
|
||||
auth required pam_env.so
|
||||
auth [default=1 ignore=ignore success=ok] pam_localuser.so
|
||||
auth sufficient pam_unix.so nullok try_first_pass
|
||||
auth requisite pam_succeed_if.so uid >= 1000 quiet_success
|
||||
auth sufficient pam_kanidm.so debug ignore_unknown_user
|
||||
auth required pam_deny.so
|
||||
|
||||
# /etc/pam.d/common-password-pc
|
||||
password requisite pam_cracklib.so
|
||||
password [default=1 ignore=ignore success=ok] pam_localuser.so
|
||||
password required pam_unix.so use_authtok nullok shadow try_first_pass
|
||||
password required pam_kanidm.so
|
||||
|
||||
# /etc/pam.d/common-session-pc
|
||||
session optional pam_systemd.so
|
||||
session required pam_limits.so
|
||||
session required pam_mkhomedir.so skel=/etc/skel/ umask=0022
|
||||
session optional pam_unix.so try_first_pass
|
||||
session optional pam_kanidm.so
|
||||
session optional pam_umask.so
|
||||
session optional pam_env.so
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Increase logging
|
||||
|
||||
For the unixd daemon, you can increase the logging with:
|
||||
|
||||
systemctl edit kanidm-unixd.service
|
||||
|
||||
And add the lines:
|
||||
|
||||
[Service]
|
||||
Environment="RUST_LOG=kanidm=debug"
|
||||
|
||||
Then restart the kanidm-unixd.service.
|
||||
|
||||
To debug the pam module interactions add `debug` to the module arguments such as:
|
||||
|
||||
auth sufficient pam_kanidm.so debug
|
||||
|
||||
### Check the socket permissions
|
||||
|
||||
Check that the /var/run/kanidm.sock is 777, and that non-root readers can see it with
|
||||
|
|
|
@ -44,6 +44,37 @@ impl KanidmAsyncClient {
|
|||
Ok(r)
|
||||
}
|
||||
|
||||
async fn perform_put_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
|
||||
.put(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);
|
||||
|
@ -187,4 +218,42 @@ impl KanidmAsyncClient {
|
|||
self.perform_delete_request(["/v1/group/", id].concat().as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_unix_cred_put(&self, id: &str, cred: &str) -> Result<(), ClientError> {
|
||||
let req = SingleStringRequest {
|
||||
value: cred.to_string(),
|
||||
};
|
||||
self.perform_put_request(
|
||||
["/v1/account/", id, "/_unix/_credential"].concat().as_str(),
|
||||
req,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(["/v1/account/", id, "/_unix/_credential"].concat().as_str())
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_unix_cred_verify(
|
||||
&self,
|
||||
id: &str,
|
||||
cred: &str,
|
||||
) -> Result<Option<UnixUserToken>, ClientError> {
|
||||
let req = SingleStringRequest {
|
||||
value: cred.to_string(),
|
||||
};
|
||||
self.perform_post_request(["/v1/account/", id, "/_unix/_auth"].concat().as_str(), req)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_group_add_members(
|
||||
&self,
|
||||
id: &str,
|
||||
members: Vec<&str>,
|
||||
) -> Result<(), ClientError> {
|
||||
let m: Vec<_> = members.iter().map(|v| (*v).to_string()).collect();
|
||||
self.perform_post_request(["/v1/group/", id, "/_attr/member"].concat().as_str(), m)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -205,7 +205,9 @@ impl KanidmClientBuilder {
|
|||
};
|
||||
|
||||
let client_builder = match &self.connect_timeout {
|
||||
Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
|
||||
Some(secs) => client_builder
|
||||
.connect_timeout(Duration::from_secs(*secs))
|
||||
.timeout(Duration::from_secs(*secs)),
|
||||
None => client_builder,
|
||||
};
|
||||
|
||||
|
@ -239,7 +241,9 @@ impl KanidmClientBuilder {
|
|||
};
|
||||
|
||||
let client_builder = match &self.connect_timeout {
|
||||
Some(secs) => client_builder.connect_timeout(Duration::from_secs(*secs)),
|
||||
Some(secs) => client_builder
|
||||
.connect_timeout(Duration::from_secs(*secs))
|
||||
.timeout(Duration::from_secs(*secs)),
|
||||
None => client_builder,
|
||||
};
|
||||
|
||||
|
@ -650,6 +654,31 @@ impl KanidmClient {
|
|||
self.perform_get_request(format!("/v1/account/{}/_unix/_token", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_put(&self, id: &str, cred: &str) -> Result<(), ClientError> {
|
||||
let req = SingleStringRequest {
|
||||
value: cred.to_string(),
|
||||
};
|
||||
self.perform_put_request(
|
||||
format!("/v1/account/{}/_unix/_credential", id).as_str(),
|
||||
req,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
|
||||
self.perform_delete_request(format!("/v1/account/{}/_unix/_credential", id).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_unix_cred_verify(
|
||||
&self,
|
||||
id: &str,
|
||||
cred: &str,
|
||||
) -> Result<Option<UnixUserToken>, ClientError> {
|
||||
let req = SingleStringRequest {
|
||||
value: cred.to_string(),
|
||||
};
|
||||
self.perform_post_request(format!("/v1/account/{}/_unix/_auth", id).as_str(), req)
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ use log::debug;
|
|||
static PORT_ALLOC: AtomicUsize = AtomicUsize::new(8080);
|
||||
static ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
||||
static ADMIN_TEST_PASSWORD_CHANGE: &str = "integration test admin new🎉";
|
||||
static UNIX_TEST_PASSWORD: &str = "unix test user password";
|
||||
|
||||
// Test external behaviorus of the service.
|
||||
|
||||
|
@ -612,6 +613,67 @@ fn test_server_rest_posix_lifecycle() {
|
|||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_server_rest_posix_auth_lifecycle() {
|
||||
run_test(|rsclient: KanidmClient| {
|
||||
let res = rsclient.auth_simple_password("admin", ADMIN_TEST_PASSWORD);
|
||||
assert!(res.is_ok());
|
||||
// Get an anon connection
|
||||
let anon_rsclient = rsclient.new_session().unwrap();
|
||||
assert!(anon_rsclient.auth_anonymous().is_ok());
|
||||
|
||||
// Not recommended in production!
|
||||
rsclient
|
||||
.idm_group_add_members("idm_admins", vec!["admin"])
|
||||
.unwrap();
|
||||
|
||||
// Setup a unix user
|
||||
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();
|
||||
|
||||
// add their password (unix self)
|
||||
rsclient
|
||||
.idm_account_unix_cred_put("posix_account", UNIX_TEST_PASSWORD)
|
||||
.unwrap();
|
||||
|
||||
// attempt to verify (good, anon-conn)
|
||||
let r1 = anon_rsclient.idm_account_unix_cred_verify("posix_account", UNIX_TEST_PASSWORD);
|
||||
match r1 {
|
||||
Ok(Some(_tok)) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// attempt to verify (bad, anon-conn)
|
||||
let r2 = anon_rsclient.idm_account_unix_cred_verify("posix_account", "ntaotnhuohtsuoehtsu");
|
||||
match r2 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
// lock? (admin-conn)
|
||||
// attempt to verify (good pw, should fail, anon-conn)
|
||||
// status? (self-conn)
|
||||
|
||||
// clear password? (unix self)
|
||||
rsclient
|
||||
.idm_account_unix_cred_delete("posix_account")
|
||||
.unwrap();
|
||||
|
||||
// attempt to verify (good pw, should fail, anon-conn)
|
||||
let r3 = anon_rsclient.idm_account_unix_cred_verify("posix_account", UNIX_TEST_PASSWORD);
|
||||
match r3 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Test the self version of the radius path.
|
||||
|
||||
// Test hitting all auth-required endpoints and assert they give unauthorized.
|
||||
|
|
|
@ -201,6 +201,8 @@ enum AccountPosix {
|
|||
Show(AccountNamedOpt),
|
||||
#[structopt(name = "set")]
|
||||
Set(AccountPosixOpt),
|
||||
#[structopt(name = "set_password")]
|
||||
SetPassword(AccountNamedOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -335,6 +337,7 @@ impl ClientOpt {
|
|||
AccountOpt::Posix(apopt) => match apopt {
|
||||
AccountPosix::Show(apo) => apo.copt.debug,
|
||||
AccountPosix::Set(apo) => apo.copt.debug,
|
||||
AccountPosix::SetPassword(apo) => apo.copt.debug,
|
||||
},
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(ano) => ano.copt.debug,
|
||||
|
@ -527,6 +530,18 @@ fn main() {
|
|||
)
|
||||
.unwrap();
|
||||
}
|
||||
AccountPosix::SetPassword(aopt) => {
|
||||
let client = aopt.copt.to_client();
|
||||
let password =
|
||||
rpassword::prompt_password_stderr("Enter new unix (sudo) password: ")
|
||||
.unwrap();
|
||||
client
|
||||
.idm_account_unix_cred_put(
|
||||
aopt.aopts.account_id.as_str(),
|
||||
password.as_str(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}, // end AccountOpt::Posix
|
||||
AccountOpt::Ssh(asopt) => match asopt {
|
||||
AccountSsh::List(aopt) => {
|
||||
|
|
|
@ -35,12 +35,19 @@ path = "src/cache_clear.rs"
|
|||
name = "kanidm_unixd_status"
|
||||
path = "src/daemon_status.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "kanidm_test_auth"
|
||||
path = "src/test_auth.rs"
|
||||
|
||||
[dependencies]
|
||||
kanidm_client = { path = "../kanidm_client", version = "0.1" }
|
||||
kanidm_proto = { path = "../kanidm_proto", version = "0.1" }
|
||||
kanidm = { path = "../kanidmd" }
|
||||
# actix = { path = "../../actix", version = "0.9" }
|
||||
actix = "0.7"
|
||||
# actix-rt = "1.0"
|
||||
toml = "0.5"
|
||||
rpassword = "0.4"
|
||||
tokio = { version = "0.2", features=["full"] }
|
||||
tokio-util = { version = "0.2", features = ["codec"] }
|
||||
futures = "0.3"
|
||||
|
|
|
@ -4,7 +4,7 @@ extern crate libnss;
|
|||
extern crate lazy_static;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon_blocking;
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, NssGroup, NssUser};
|
||||
|
||||
use libnss::group::{Group, GroupHooks};
|
||||
|
@ -18,8 +18,11 @@ libnss_passwd_hooks!(kanidm, KanidmPasswd);
|
|||
|
||||
impl PasswdHooks for KanidmPasswd {
|
||||
fn get_all_entries() -> Response<Vec<Passwd>> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssAccounts;
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccounts(l) => l.into_iter().map(passwd_from_nssuser).collect(),
|
||||
_ => Vec::new(),
|
||||
|
@ -29,8 +32,11 @@ impl PasswdHooks for KanidmPasswd {
|
|||
}
|
||||
|
||||
fn get_entry_by_uid(uid: libc::uid_t) -> Response<Passwd> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssAccountByUid(uid);
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccount(opt) => opt
|
||||
.map(passwd_from_nssuser)
|
||||
|
@ -42,8 +48,11 @@ impl PasswdHooks for KanidmPasswd {
|
|||
}
|
||||
|
||||
fn get_entry_by_name(name: String) -> Response<Passwd> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssAccountByName(name);
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssAccount(opt) => opt
|
||||
.map(passwd_from_nssuser)
|
||||
|
@ -60,8 +69,11 @@ libnss_group_hooks!(kanidm, KanidmGroup);
|
|||
|
||||
impl GroupHooks for KanidmGroup {
|
||||
fn get_all_entries() -> Response<Vec<Group>> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssGroups;
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroups(l) => l.into_iter().map(group_from_nssgroup).collect(),
|
||||
_ => Vec::new(),
|
||||
|
@ -71,8 +83,11 @@ impl GroupHooks for KanidmGroup {
|
|||
}
|
||||
|
||||
fn get_entry_by_gid(gid: libc::gid_t) -> Response<Group> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssGroupByGid(gid);
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroup(opt) => opt
|
||||
.map(group_from_nssgroup)
|
||||
|
@ -84,8 +99,11 @@ impl GroupHooks for KanidmGroup {
|
|||
}
|
||||
|
||||
fn get_entry_by_name(name: String) -> Response<Group> {
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
let req = ClientRequest::NssGroupByName(name);
|
||||
call_daemon_blocking(DEFAULT_SOCK_PATH, req)
|
||||
call_daemon_blocking(cfg.sock_path.as_str(), req)
|
||||
.map(|r| match r {
|
||||
ClientResponse::NssGroup(opt) => opt
|
||||
.map(group_from_nssgroup)
|
||||
|
|
|
@ -11,3 +11,6 @@ path = "src/lib.rs"
|
|||
|
||||
[dependencies]
|
||||
kanidm_unix_int = { path = "../", version = "0.1" }
|
||||
futures = "0.3"
|
||||
tokio = { version = "0.2", features=["full"] }
|
||||
libc = "0.2"
|
||||
|
|
|
@ -1,7 +1,296 @@
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
extern crate libc;
|
||||
|
||||
mod pam;
|
||||
use crate::pam::constants::*;
|
||||
use crate::pam::conv::PamConv;
|
||||
use crate::pam::module::{PamHandle, PamHooks};
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::ffi::CStr;
|
||||
// use std::os::raw::c_char;
|
||||
|
||||
// use futures::executor::block_on;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Options {
|
||||
debug: bool,
|
||||
use_first_pass: bool,
|
||||
ignore_unknown_user: bool,
|
||||
}
|
||||
|
||||
impl TryFrom<&Vec<&CStr>> for Options {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(args: &Vec<&CStr>) -> Result<Self, Self::Error> {
|
||||
let opts: Result<BTreeSet<&str>, _> = args.iter().map(|cs| cs.to_str()).collect();
|
||||
let gopts = match opts {
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
println!("Error in module args -> {:?}", e);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Options {
|
||||
debug: gopts.contains("debug"),
|
||||
use_first_pass: gopts.contains("use_first_pass"),
|
||||
ignore_unknown_user: gopts.contains("ignore_unknown_user"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cfg() -> Result<KanidmUnixdConfig, PamResultCode> {
|
||||
KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.map_err(|_| PamResultCode::PAM_SERVICE_ERR)
|
||||
}
|
||||
|
||||
struct PamKanidm;
|
||||
pam_hooks!(PamKanidm);
|
||||
|
||||
impl PamHooks for PamKanidm {
|
||||
fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("acct_mgmt");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
|
||||
let account_id = match pamh.get_user(None) {
|
||||
Ok(aid) => aid,
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("Error get_user -> {:?}", e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
let cfg = match get_cfg() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let req = ClientRequest::PamAccountAllowed(account_id);
|
||||
// PamResultCode::PAM_IGNORE
|
||||
|
||||
let mut rt = match Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
match rt.block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
// println!("PAM_SUCCESS");
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
// println!("PAM_IGNORE");
|
||||
PamResultCode::PAM_AUTH_ERR
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
if opts.ignore_unknown_user {
|
||||
PamResultCode::PAM_IGNORE
|
||||
} else {
|
||||
PamResultCode::PAM_USER_UNKNOWN
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", r);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", e);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sm_authenticate(pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_authenticate");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
|
||||
let account_id = match pamh.get_user(None) {
|
||||
Ok(aid) => aid,
|
||||
Err(e) => {
|
||||
println!("Error get_user -> {:?}", e);
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
let authtok = match pamh.get_authtok() {
|
||||
Ok(atok) => atok,
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("Error get_authtok -> {:?}", e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
let authtok = match authtok {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
if opts.use_first_pass {
|
||||
if opts.debug {
|
||||
println!("Don't have an authtok, returning PAM_AUTH_ERR");
|
||||
}
|
||||
return PamResultCode::PAM_AUTH_ERR;
|
||||
} else {
|
||||
let conv = match pamh.get_item::<PamConv>() {
|
||||
Ok(conv) => conv,
|
||||
Err(err) => {
|
||||
if opts.debug {
|
||||
println!("Couldn't get pam_conv");
|
||||
}
|
||||
return err;
|
||||
}
|
||||
};
|
||||
match conv.send(PAM_PROMPT_ECHO_OFF, "Password: ") {
|
||||
Ok(password) => match password {
|
||||
Some(pw) => pw,
|
||||
None => {
|
||||
if opts.debug {
|
||||
println!("No password");
|
||||
}
|
||||
return PamResultCode::PAM_CRED_INSUFFICIENT;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
if opts.debug {
|
||||
println!("Couldn't get password");
|
||||
}
|
||||
return err;
|
||||
}
|
||||
}
|
||||
} // end opts.use_first_pass
|
||||
}
|
||||
};
|
||||
|
||||
let cfg = match get_cfg() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let req = ClientRequest::PamAuthenticate(account_id, authtok);
|
||||
|
||||
let mut rt = match Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
match rt.block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
// println!("PAM_SUCCESS");
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
// println!("PAM_AUTH_ERR");
|
||||
PamResultCode::PAM_AUTH_ERR
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
// println!("PAM_USER_UNKNOWN");
|
||||
if opts.ignore_unknown_user {
|
||||
PamResultCode::PAM_IGNORE
|
||||
} else {
|
||||
PamResultCode::PAM_USER_UNKNOWN
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", r);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
if opts.debug {
|
||||
println!("PAM_IGNORE -> {:?}", e);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sm_chauthtok(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_chauthtok");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
fn sm_close_session(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_close_session");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
|
||||
fn sm_open_session(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_open_session");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
|
||||
fn sm_setcred(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode {
|
||||
let opts = match Options::try_from(&args) {
|
||||
Ok(o) => o,
|
||||
Err(_) => return PamResultCode::PAM_SERVICE_ERR,
|
||||
};
|
||||
|
||||
if opts.debug {
|
||||
println!("sm_setcred");
|
||||
println!("args -> {:?}", args);
|
||||
println!("opts -> {:?}", opts);
|
||||
}
|
||||
PamResultCode::PAM_SUCCESS
|
||||
}
|
||||
}
|
||||
|
|
97
kanidm_unix_int/pam_kanidm/src/pam/constants.rs
Normal file
97
kanidm_unix_int/pam_kanidm/src/pam/constants.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use libc::{c_int, c_uint};
|
||||
|
||||
// TODO: Import constants from C header file at compile time.
|
||||
|
||||
pub type PamFlag = c_uint;
|
||||
pub type PamItemType = c_int;
|
||||
pub type PamMessageStyle = c_int;
|
||||
pub type AlwaysZero = c_int;
|
||||
|
||||
// The Linux-PAM flags
|
||||
// see /usr/include/security/_pam_types.h
|
||||
pub const _PAM_SILENT: PamFlag = 0x8000;
|
||||
pub const _PAM_DISALLOW_NULL_AUTHTOK: PamFlag = 0x0001;
|
||||
pub const _PAM_ESTABLISH_CRED: PamFlag = 0x0002;
|
||||
pub const _PAM_DELETE_CRED: PamFlag = 0x0004;
|
||||
pub const _PAM_REINITIALIZE_CRED: PamFlag = 0x0008;
|
||||
pub const _PAM_REFRESH_CRED: PamFlag = 0x0010;
|
||||
pub const _PAM_CHANGE_EXPIRED_AUTHTOK: PamFlag = 0x0020;
|
||||
|
||||
// The Linux-PAM item types
|
||||
// see /usr/include/security/_pam_types.h
|
||||
/// The service name
|
||||
pub const PAM_SERVICE: PamItemType = 1;
|
||||
/// The user name
|
||||
pub const PAM_USER: PamItemType = 2;
|
||||
/// The tty name
|
||||
pub const PAM_TTY: PamItemType = 3;
|
||||
/// The remote host name
|
||||
pub const PAM_RHOST: PamItemType = 4;
|
||||
/// The pam_conv structure
|
||||
pub const PAM_CONV: PamItemType = 5;
|
||||
/// The authentication token (password)
|
||||
pub const PAM_AUTHTOK: PamItemType = 6;
|
||||
/// The old authentication token
|
||||
pub const PAM_OLDAUTHTOK: PamItemType = 7;
|
||||
/// The remote user name
|
||||
pub const PAM_RUSER: PamItemType = 8;
|
||||
/// the prompt for getting a username
|
||||
pub const PAM_USER_PROMPT: PamItemType = 9;
|
||||
/* Linux-PAM :extensionsPamItemType = */
|
||||
/// app supplied function to override failure delays
|
||||
pub const _PAM_FAIL_DELAY: PamItemType = 10;
|
||||
/// X :display name
|
||||
pub const _PAM_XDISPLAY: PamItemType = 11;
|
||||
/// X :server authentication data
|
||||
pub const _PAM_XAUTHDATA: PamItemType = 12;
|
||||
/// The type for pam_get_authtok
|
||||
pub const _PAM_AUTHTOK_TYPE: PamItemType = 13;
|
||||
|
||||
// Message styles
|
||||
pub const PAM_PROMPT_ECHO_OFF: PamMessageStyle = 1;
|
||||
pub const _PAM_PROMPT_ECHO_ON: PamMessageStyle = 2;
|
||||
pub const _PAM_ERROR_MSG: PamMessageStyle = 3;
|
||||
pub const _PAM_TEXT_INFO: PamMessageStyle = 4;
|
||||
/// yes/no/maybe conditionals
|
||||
pub const _PAM_RADIO_TYPE: PamMessageStyle = 5;
|
||||
pub const _PAM_BINARY_PROMPT: PamMessageStyle = 7;
|
||||
|
||||
// The Linux-PAM return values
|
||||
// see /usr/include/security/_pam_types.h
|
||||
#[allow(non_camel_case_types, dead_code)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub enum PamResultCode {
|
||||
PAM_SUCCESS = 0,
|
||||
PAM_OPEN_ERR = 1,
|
||||
PAM_SYMBOL_ERR = 2,
|
||||
PAM_SERVICE_ERR = 3,
|
||||
PAM_SYSTEM_ERR = 4,
|
||||
PAM_BUF_ERR = 5,
|
||||
PAM_PERM_DENIED = 6,
|
||||
PAM_AUTH_ERR = 7,
|
||||
PAM_CRED_INSUFFICIENT = 8,
|
||||
PAM_AUTHINFO_UNAVAIL = 9,
|
||||
PAM_USER_UNKNOWN = 10,
|
||||
PAM_MAXTRIES = 11,
|
||||
PAM_NEW_AUTHTOK_REQD = 12,
|
||||
PAM_ACCT_EXPIRED = 13,
|
||||
PAM_SESSION_ERR = 14,
|
||||
PAM_CRED_UNAVAIL = 15,
|
||||
PAM_CRED_EXPIRED = 16,
|
||||
PAM_CRED_ERR = 17,
|
||||
PAM_NO_MODULE_DATA = 18,
|
||||
PAM_CONV_ERR = 19,
|
||||
PAM_AUTHTOK_ERR = 20,
|
||||
PAM_AUTHTOK_RECOVERY_ERR = 21,
|
||||
PAM_AUTHTOK_LOCK_BUSY = 22,
|
||||
PAM_AUTHTOK_DISABLE_AGING = 23,
|
||||
PAM_TRY_AGAIN = 24,
|
||||
PAM_IGNORE = 25,
|
||||
PAM_ABORT = 26,
|
||||
PAM_AUTHTOK_EXPIRED = 27,
|
||||
PAM_MODULE_UNKNOWN = 28,
|
||||
PAM_BAD_ITEM = 29,
|
||||
PAM_CONV_AGAIN = 30,
|
||||
PAM_INCOMPLETE = 31,
|
||||
}
|
85
kanidm_unix_int/pam_kanidm/src/pam/conv.rs
Normal file
85
kanidm_unix_int/pam_kanidm/src/pam/conv.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use libc::{c_char, c_int};
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::ptr;
|
||||
|
||||
use crate::pam::constants::PamResultCode;
|
||||
use crate::pam::constants::*;
|
||||
use crate::pam::module::{PamItem, PamResult};
|
||||
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum AppDataPtr {}
|
||||
|
||||
#[repr(C)]
|
||||
struct PamMessage {
|
||||
msg_style: PamMessageStyle,
|
||||
msg: *const c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct PamResponse {
|
||||
resp: *const c_char,
|
||||
resp_retcode: AlwaysZero,
|
||||
}
|
||||
|
||||
/// `PamConv` acts as a channel for communicating with user.
|
||||
///
|
||||
/// Communication is mediated by the pam client (the application that invoked
|
||||
/// pam). Messages sent will be relayed to the user by the client, and response
|
||||
/// will be relayed back.
|
||||
#[repr(C)]
|
||||
pub struct PamConv {
|
||||
conv: extern "C" fn(
|
||||
num_msg: c_int,
|
||||
pam_message: &&PamMessage,
|
||||
pam_response: &mut *const PamResponse,
|
||||
appdata_ptr: *const AppDataPtr,
|
||||
) -> PamResultCode,
|
||||
appdata_ptr: *const AppDataPtr,
|
||||
}
|
||||
|
||||
impl PamConv {
|
||||
/// Sends a message to the pam client.
|
||||
///
|
||||
/// This will typically result in the user seeing a message or a prompt.
|
||||
/// There are several message styles available:
|
||||
///
|
||||
/// - PAM_PROMPT_ECHO_OFF
|
||||
/// - PAM_PROMPT_ECHO_ON
|
||||
/// - PAM_ERROR_MSG
|
||||
/// - PAM_TEXT_INFO
|
||||
/// - PAM_RADIO_TYPE
|
||||
/// - PAM_BINARY_PROMPT
|
||||
///
|
||||
/// Note that the user experience will depend on how the client implements
|
||||
/// these message styles - and not all applications implement all message
|
||||
/// styles.
|
||||
pub fn send(&self, style: PamMessageStyle, msg: &str) -> PamResult<Option<String>> {
|
||||
let mut resp_ptr: *const PamResponse = ptr::null();
|
||||
let msg_cstr = CString::new(msg).unwrap();
|
||||
let msg = PamMessage {
|
||||
msg_style: style,
|
||||
msg: msg_cstr.as_ptr(),
|
||||
};
|
||||
|
||||
let ret = (self.conv)(1, &&msg, &mut resp_ptr, self.appdata_ptr);
|
||||
|
||||
if PamResultCode::PAM_SUCCESS == ret {
|
||||
// PamResponse.resp is null for styles that don't return user input like PAM_TEXT_INFO
|
||||
let response = unsafe { (*resp_ptr).resp };
|
||||
if response.is_null() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let bytes = unsafe { CStr::from_ptr(response).to_bytes() };
|
||||
Ok(String::from_utf8(bytes.to_vec()).ok())
|
||||
}
|
||||
} else {
|
||||
Err(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PamItem for PamConv {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_CONV
|
||||
}
|
||||
}
|
78
kanidm_unix_int/pam_kanidm/src/pam/items.rs
Normal file
78
kanidm_unix_int/pam_kanidm/src/pam/items.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use crate::pam::constants::{
|
||||
PamItemType, PAM_AUTHTOK, PAM_OLDAUTHTOK, PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER,
|
||||
PAM_USER_PROMPT,
|
||||
};
|
||||
pub use crate::pam::conv::PamConv;
|
||||
use crate::pam::module::PamItem;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamService {}
|
||||
|
||||
impl PamItem for PamService {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_SERVICE
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamUser {}
|
||||
|
||||
impl PamItem for PamUser {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_USER
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamUserPrompt {}
|
||||
|
||||
impl PamItem for PamUserPrompt {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_USER_PROMPT
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamTty {}
|
||||
|
||||
impl PamItem for PamTty {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_TTY
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamRUser {}
|
||||
|
||||
impl PamItem for PamRUser {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_RUSER
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamRHost {}
|
||||
|
||||
impl PamItem for PamRHost {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_RHOST
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamAuthTok {}
|
||||
|
||||
impl PamItem for PamAuthTok {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_AUTHTOK
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct PamOldAuthTok {}
|
||||
|
||||
impl PamItem for PamOldAuthTok {
|
||||
fn item_type() -> PamItemType {
|
||||
PAM_OLDAUTHTOK
|
||||
}
|
||||
}
|
115
kanidm_unix_int/pam_kanidm/src/pam/macros.rs
Normal file
115
kanidm_unix_int/pam_kanidm/src/pam/macros.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
/// Macro to generate the `extern "C"` entrypoint bindings needed by PAM
|
||||
///
|
||||
/// You can call `pam_hooks!(SomeType);` for any type that implements `PamHooks`
|
||||
///
|
||||
/// ## Examples:
|
||||
///
|
||||
/// Here is full example of a PAM module that would authenticate and authorize everybody:
|
||||
///
|
||||
/// ```
|
||||
/// #[macro_use] extern crate pam;
|
||||
///
|
||||
/// use pam::module::{PamHooks, PamHandle};
|
||||
/// use pam::constants::{PamResultCode, PamFlag};
|
||||
/// use std::ffi::CStr;
|
||||
///
|
||||
/// # fn main() {}
|
||||
/// struct MyPamModule;
|
||||
/// pam_hooks!(MyPamModule);
|
||||
///
|
||||
/// impl PamHooks for MyPamModule {
|
||||
/// fn sm_authenticate(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
/// println!("Everybody is authenticated!");
|
||||
/// PamResultCode::PAM_SUCCESS
|
||||
/// }
|
||||
///
|
||||
/// fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
/// println!("Everybody is authorized!");
|
||||
/// PamResultCode::PAM_SUCCESS
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! pam_hooks {
|
||||
($ident:ident) => {
|
||||
pub use self::pam_hooks_scope::*;
|
||||
mod pam_hooks_scope {
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use $crate::pam::constants::{PamFlag, PamResultCode};
|
||||
use $crate::pam::module::{PamHandle, PamHooks};
|
||||
|
||||
fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
|
||||
(0..argc)
|
||||
.map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_acct_mgmt(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::acct_mgmt(pamh, args, flags)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_authenticate(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::sm_authenticate(pamh, args, flags)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_chauthtok(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::sm_chauthtok(pamh, args, flags)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_close_session(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::sm_close_session(pamh, args, flags)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_open_session(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::sm_open_session(pamh, args, flags)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pam_sm_setcred(
|
||||
pamh: &PamHandle,
|
||||
flags: PamFlag,
|
||||
argc: c_int,
|
||||
argv: *const *const c_char,
|
||||
) -> PamResultCode {
|
||||
let args = extract_argv(argc, argv);
|
||||
super::$ident::sm_setcred(pamh, args, flags)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
32
kanidm_unix_int/pam_kanidm/src/pam/mod.rs
Executable file
32
kanidm_unix_int/pam_kanidm/src/pam/mod.rs
Executable file
|
@ -0,0 +1,32 @@
|
|||
//! Interface to the pluggable authentication module framework (PAM).
|
||||
//!
|
||||
//! The goal of this library is to provide a type-safe API that can be used to
|
||||
//! interact with PAM. The library is incomplete - currently it supports
|
||||
//! a subset of functions for use in a pam authentication module. A pam module
|
||||
//! is a shared library that is invoked to authenticate a user, or to perform
|
||||
//! other functions.
|
||||
//!
|
||||
//! For general information on writing pam modules, see
|
||||
//! [The Linux-PAM Module Writers' Guide][module-guide]
|
||||
//!
|
||||
//! [module-guide]: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_MWG.html
|
||||
//!
|
||||
//! A typical authentication module will define an external function called
|
||||
//! `pam_sm_authenticate()`, which will use functions in this library to
|
||||
//! interrogate the program that requested authentication for more information,
|
||||
//! and to render a result. For a working example that uses this library, see
|
||||
//! [toznyauth-pam][].
|
||||
//!
|
||||
//! [toznyauth-pam]: https://github.com/tozny/toznyauth-pam
|
||||
//!
|
||||
//! Note that constants that are normally read from pam header files are
|
||||
//! hard-coded in the `constants` module. The values there are taken from
|
||||
//! a Linux system. That means that it might take some work to get this library
|
||||
//! to work on other platforms.
|
||||
|
||||
pub mod constants;
|
||||
pub mod conv;
|
||||
pub mod items;
|
||||
#[doc(hidden)]
|
||||
pub mod macros;
|
||||
pub mod module;
|
257
kanidm_unix_int/pam_kanidm/src/pam/module.rs
Executable file
257
kanidm_unix_int/pam_kanidm/src/pam/module.rs
Executable file
|
@ -0,0 +1,257 @@
|
|||
//! Functions for use in pam modules.
|
||||
|
||||
use libc::c_char;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::{mem, ptr};
|
||||
|
||||
use crate::pam::constants::{PamFlag, PamItemType, PamResultCode, PAM_AUTHTOK};
|
||||
|
||||
/// Opaque type, used as a pointer when making pam API calls.
|
||||
///
|
||||
/// A module is invoked via an external function such as `pam_sm_authenticate`.
|
||||
/// Such a call provides a pam handle pointer. The same pointer should be given
|
||||
/// as an argument when making API calls.
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum PamHandle {}
|
||||
|
||||
#[allow(missing_copy_implementations)]
|
||||
enum PamItemT {}
|
||||
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum PamDataT {}
|
||||
|
||||
#[link(name = "pam")]
|
||||
extern "C" {
|
||||
fn pam_get_data(
|
||||
pamh: *const PamHandle,
|
||||
module_data_name: *const c_char,
|
||||
data: &mut *const PamDataT,
|
||||
) -> PamResultCode;
|
||||
|
||||
fn pam_set_data(
|
||||
pamh: *const PamHandle,
|
||||
module_data_name: *const c_char,
|
||||
data: *mut PamDataT,
|
||||
cleanup: extern "C" fn(
|
||||
pamh: *const PamHandle,
|
||||
data: *mut PamDataT,
|
||||
error_status: PamResultCode,
|
||||
),
|
||||
) -> PamResultCode;
|
||||
|
||||
fn pam_get_item(
|
||||
pamh: *const PamHandle,
|
||||
item_type: PamItemType,
|
||||
item: &mut *const PamItemT,
|
||||
) -> PamResultCode;
|
||||
|
||||
fn pam_set_item(pamh: *mut PamHandle, item_type: PamItemType, item: &PamItemT)
|
||||
-> PamResultCode;
|
||||
|
||||
fn pam_get_user(
|
||||
pamh: *const PamHandle,
|
||||
user: &*mut c_char,
|
||||
prompt: *const c_char,
|
||||
) -> PamResultCode;
|
||||
}
|
||||
|
||||
pub extern "C" fn cleanup<T>(_: *const PamHandle, c_data: *mut PamDataT, _: PamResultCode) {
|
||||
unsafe {
|
||||
let c_data = Box::from_raw(c_data);
|
||||
let data: Box<T> = mem::transmute(c_data);
|
||||
mem::drop(data);
|
||||
}
|
||||
}
|
||||
|
||||
pub type PamResult<T> = Result<T, PamResultCode>;
|
||||
|
||||
/// Type-level mapping for safely retrieving values with `get_item`.
|
||||
///
|
||||
/// See `pam_get_item` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub trait PamItem {
|
||||
/// Maps a Rust type to a pam constant.
|
||||
///
|
||||
/// For example, the type PamConv maps to the constant PAM_CONV. The pam
|
||||
/// API contract specifies that when the API function `pam_get_item` is
|
||||
/// called with the constant PAM_CONV, it will return a value of type
|
||||
/// `PamConv`.
|
||||
fn item_type() -> PamItemType;
|
||||
}
|
||||
|
||||
impl PamHandle {
|
||||
/// Gets some value, identified by `key`, that has been set by the module
|
||||
/// previously.
|
||||
///
|
||||
/// See `pam_get_data` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub unsafe fn get_data<'a, T>(&'a self, key: &str) -> PamResult<&'a T> {
|
||||
let c_key = CString::new(key).unwrap().as_ptr();
|
||||
let mut ptr: *const PamDataT = ptr::null();
|
||||
let res = pam_get_data(self, c_key, &mut ptr);
|
||||
if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() {
|
||||
let typed_ptr: *const T = mem::transmute(ptr);
|
||||
let data: &T = &*typed_ptr;
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a value that can be retrieved later with `get_data`. The value lives
|
||||
/// as long as the current pam cycle.
|
||||
///
|
||||
/// See `pam_set_data` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub fn set_data<T>(&self, key: &str, data: Box<T>) -> PamResult<()> {
|
||||
let c_key = CString::new(key).unwrap().as_ptr();
|
||||
let res = unsafe {
|
||||
let c_data: Box<PamDataT> = mem::transmute(data);
|
||||
let c_data = Box::into_raw(c_data);
|
||||
pam_set_data(self, c_key, c_data, cleanup::<T>)
|
||||
};
|
||||
if PamResultCode::PAM_SUCCESS == res {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a value that has been set, possibly by the pam client. This is
|
||||
/// particularly useful for getting a `PamConv` reference.
|
||||
///
|
||||
/// See `pam_get_item` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub fn get_item<'a, T: PamItem>(&self) -> PamResult<&'a T> {
|
||||
let mut ptr: *const PamItemT = ptr::null();
|
||||
let (res, item) = unsafe {
|
||||
let r = pam_get_item(self, T::item_type(), &mut ptr);
|
||||
let typed_ptr: *const T = mem::transmute(ptr);
|
||||
let t: &T = &*typed_ptr;
|
||||
(r, t)
|
||||
};
|
||||
if PamResultCode::PAM_SUCCESS == res {
|
||||
Ok(item)
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets a value in the pam context. The value can be retrieved using
|
||||
/// `get_item`.
|
||||
///
|
||||
/// Note that all items are strings, except `PAM_CONV` and `PAM_FAIL_DELAY`.
|
||||
///
|
||||
/// See `pam_set_item` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub fn set_item_str<T: PamItem>(&mut self, item: &str) -> PamResult<()> {
|
||||
let c_item = CString::new(item).unwrap().as_ptr();
|
||||
|
||||
let res = unsafe {
|
||||
pam_set_item(
|
||||
self,
|
||||
T::item_type(),
|
||||
// unwrapping is okay here, as c_item will not be a NULL
|
||||
// pointer
|
||||
(c_item as *const PamItemT).as_ref().unwrap(),
|
||||
)
|
||||
};
|
||||
if PamResultCode::PAM_SUCCESS == res {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the name of the user who is authenticating or logging in.
|
||||
///
|
||||
/// This is really a specialization of `get_item`.
|
||||
///
|
||||
/// See `pam_get_user` in
|
||||
/// http://www.linux-pam.org/Linux-PAM-html/mwg-expected-by-module-item.html
|
||||
pub fn get_user(&self, prompt: Option<&str>) -> PamResult<String> {
|
||||
let ptr: *mut c_char = ptr::null_mut();
|
||||
let c_prompt = match prompt {
|
||||
Some(p) => CString::new(p).unwrap().as_ptr(),
|
||||
None => ptr::null(),
|
||||
};
|
||||
let res = unsafe { pam_get_user(self, &ptr, c_prompt) };
|
||||
if PamResultCode::PAM_SUCCESS == res && !ptr.is_null() {
|
||||
let const_ptr = ptr as *const c_char;
|
||||
let bytes = unsafe { CStr::from_ptr(const_ptr).to_bytes() };
|
||||
String::from_utf8(bytes.to_vec()).map_err(|_| PamResultCode::PAM_CONV_ERR)
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_authtok(&self) -> PamResult<Option<String>> {
|
||||
let mut ptr: *const PamItemT = ptr::null();
|
||||
let (res, item) = unsafe {
|
||||
let r = pam_get_item(self, PAM_AUTHTOK, &mut ptr);
|
||||
let t = if PamResultCode::PAM_SUCCESS == r && !ptr.is_null() {
|
||||
let typed_ptr: *const c_char = mem::transmute(ptr);
|
||||
Some(CStr::from_ptr(typed_ptr).to_string_lossy().into_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(r, t)
|
||||
};
|
||||
if PamResultCode::PAM_SUCCESS == res {
|
||||
Ok(item)
|
||||
} else {
|
||||
Err(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides functions that are invoked by the entrypoints generated by the
|
||||
/// [`pam_hooks!` macro](../macro.pam_hooks.html).
|
||||
///
|
||||
/// All of hooks are ignored by PAM dispatch by default given the default return value of `PAM_IGNORE`.
|
||||
/// Override any functions that you want to handle with your module. See `man pam(3)`.
|
||||
#[allow(unused_variables)]
|
||||
pub trait PamHooks {
|
||||
/// This function performs the task of establishing whether the user is permitted to gain access at
|
||||
/// this time. It should be understood that the user has previously been validated by an
|
||||
/// authentication module. This function checks for other things. Such things might be: the time of
|
||||
/// day or the date, the terminal line, remote hostname, etc. This function may also determine
|
||||
/// things like the expiration on passwords, and respond that the user change it before continuing.
|
||||
fn acct_mgmt(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
/// This function performs the task of authenticating the user.
|
||||
fn sm_authenticate(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
/// This function is used to (re-)set the authentication token of the user.
|
||||
///
|
||||
/// The PAM library calls this function twice in succession. The first time with
|
||||
/// PAM_PRELIM_CHECK and then, if the module does not return PAM_TRY_AGAIN, subsequently with
|
||||
/// PAM_UPDATE_AUTHTOK. It is only on the second call that the authorization token is
|
||||
/// (possibly) changed.
|
||||
fn sm_chauthtok(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
/// This function is called to terminate a session.
|
||||
fn sm_close_session(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
/// This function is called to commence a session.
|
||||
fn sm_open_session(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
|
||||
/// This function performs the task of altering the credentials of the user with respect to the
|
||||
/// corresponding authorization scheme. Generally, an authentication module may have access to more
|
||||
/// information about a user than their authentication token. This function is used to make such
|
||||
/// information available to the application. It should only be called after the user has been
|
||||
/// authenticated but before a session has been established.
|
||||
fn sm_setcred(pamh: &PamHandle, args: Vec<&CStr>, flags: PamFlag) -> PamResultCode {
|
||||
PamResultCode::PAM_IGNORE
|
||||
}
|
||||
}
|
45
kanidm_unix_int/pam_tester/Cargo.lock
generated
Normal file
45
kanidm_unix_int/pam_tester/Cargo.lock
generated
Normal file
|
@ -0,0 +1,45 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "pam"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pam-sys 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"users 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam-sys"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pam_tester"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pam 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[metadata]
|
||||
"checksum libc 0.2.67 (registry+https://github.com/rust-lang/crates.io-index)" = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
|
||||
"checksum pam 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa2bdc959c201c047004a1420a92aaa1dd1a6b64d5ef333aa3a4ac764fb93097"
|
||||
"checksum pam-sys 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "cd4858311a097f01a0006ef7d0cd50bca81ec430c949d7bf95cbefd202282434"
|
||||
"checksum users 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7fed7d0912567d35f88010c23dbaf865e9da8b5227295e8dc0f2fdd109155ab7"
|
12
kanidm_unix_int/pam_tester/Cargo.toml
Normal file
12
kanidm_unix_int/pam_tester/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "pam_tester"
|
||||
version = "0.1.0"
|
||||
authors = ["William Brown <william@blackhats.net.au>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
pam = "0.7.0"
|
||||
|
||||
[workspace]
|
23
kanidm_unix_int/pam_tester/src/main.rs
Normal file
23
kanidm_unix_int/pam_tester/src/main.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
extern crate pam;
|
||||
pub fn main() {
|
||||
let service = "pam_test";
|
||||
let user = "testuser";
|
||||
let password = "eti8aoshaigeeboh1ohF7rieba0quaThesoivae0";
|
||||
|
||||
let mut auth = pam::Authenticator::with_password(service).unwrap();
|
||||
auth.get_handler().set_credentials(user, password);
|
||||
let r = auth.authenticate();
|
||||
println!("auth -> {:?}", r);
|
||||
if r.is_ok() {
|
||||
println!("Successfully authenticated!");
|
||||
let r = auth.open_session();
|
||||
println!("session -> {:?}", r);
|
||||
if r.is_ok() {
|
||||
println!("Successfully opened session!");
|
||||
} else {
|
||||
println!("Session failed =/");
|
||||
}
|
||||
} else {
|
||||
println!("Authentication failed =/");
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use kanidm_client::asynchronous::KanidmAsyncClient;
|
|||
use kanidm_client::ClientError;
|
||||
use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken};
|
||||
use reqwest::StatusCode;
|
||||
use std::collections::BTreeSet;
|
||||
use std::ops::Add;
|
||||
use std::string::ToString;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
@ -26,6 +27,7 @@ pub struct CacheLayer {
|
|||
db: Db,
|
||||
client: KanidmAsyncClient,
|
||||
state: Mutex<CacheState>,
|
||||
pam_allow_groups: BTreeSet<String>,
|
||||
timeout_seconds: u64,
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,7 @@ impl CacheLayer {
|
|||
timeout_seconds: u64,
|
||||
//
|
||||
client: KanidmAsyncClient,
|
||||
pam_allow_groups: Vec<String>,
|
||||
) -> Result<Self, ()> {
|
||||
let db = Db::new(path)?;
|
||||
|
||||
|
@ -63,6 +66,7 @@ impl CacheLayer {
|
|||
client: client,
|
||||
state: Mutex::new(CacheState::OfflineNextCheck(SystemTime::now())),
|
||||
timeout_seconds: timeout_seconds,
|
||||
pam_allow_groups: pam_allow_groups.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -209,6 +213,20 @@ impl CacheLayer {
|
|||
dbtxn.delete_group(g_uuid).and_then(|_| dbtxn.commit())
|
||||
}
|
||||
|
||||
fn set_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result<(), ()> {
|
||||
let dbtxn = self.db.write();
|
||||
dbtxn
|
||||
.update_account_password(a_uuid, cred)
|
||||
.and_then(|x| dbtxn.commit().map(|_| x))
|
||||
}
|
||||
|
||||
fn check_cache_userpassword(&self, a_uuid: &str, cred: &str) -> Result<bool, ()> {
|
||||
let dbtxn = self.db.write();
|
||||
dbtxn
|
||||
.check_account_password(a_uuid, cred)
|
||||
.and_then(|x| dbtxn.commit().map(|_| x))
|
||||
}
|
||||
|
||||
async fn refresh_usertoken(
|
||||
&self,
|
||||
account_id: &Id,
|
||||
|
@ -234,12 +252,28 @@ impl CacheLayer {
|
|||
.await;
|
||||
Ok(token)
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Some(OperationError::NotAuthenticated),
|
||||
) => {
|
||||
error!("transport unauthenticated, moving to offline");
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
Ok(token)
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
)
|
||||
| ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::InvalidAccountState(_)),
|
||||
) => {
|
||||
// We wele able to contact the server but the entry has been removed.
|
||||
debug!("entry has been removed, clearing from cache ...");
|
||||
// We wele able to contact the server but the entry has been removed, or
|
||||
// is not longer a valid posix account.
|
||||
debug!("entry has been removed or is no longer a valid posix account, clearing from cache ...");
|
||||
token
|
||||
.map(|tok| self.delete_cache_usertoken(&tok.uuid))
|
||||
// Now an option<result<t, _>>
|
||||
|
@ -250,7 +284,7 @@ impl CacheLayer {
|
|||
er => {
|
||||
error!("client error -> {:?}", er);
|
||||
// Some other transient error, continue with the token.
|
||||
Err(())
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -282,11 +316,26 @@ impl CacheLayer {
|
|||
.await;
|
||||
Ok(token)
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Some(OperationError::NotAuthenticated),
|
||||
) => {
|
||||
error!("transport unauthenticated, moving to offline");
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
Ok(token)
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
)
|
||||
| ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::InvalidAccountState(_)),
|
||||
) => {
|
||||
debug!("entry has been removed, clearing from cache ...");
|
||||
debug!("entry has been removed or is no longer a valid posix group, clearing from cache ...");
|
||||
token
|
||||
.map(|tok| self.delete_cache_grouptoken(&tok.uuid))
|
||||
// Now an option<result<t, _>>
|
||||
|
@ -297,7 +346,7 @@ impl CacheLayer {
|
|||
er => {
|
||||
error!("client error -> {:?}", er);
|
||||
// Some other transient error, continue with the token.
|
||||
Err(())
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -497,6 +546,135 @@ impl CacheLayer {
|
|||
self.get_nssgroup(Id::Gid(gid)).await
|
||||
}
|
||||
|
||||
async fn online_account_authenticate(
|
||||
&self,
|
||||
token: &Option<UnixUserToken>,
|
||||
account_id: &str,
|
||||
cred: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
debug!("Attempt online password check");
|
||||
// We are online, attempt the pw to the server.
|
||||
match self
|
||||
.client
|
||||
.idm_account_unix_cred_verify(account_id, cred)
|
||||
.await
|
||||
{
|
||||
Ok(Some(n_tok)) => {
|
||||
debug!("online password check success.");
|
||||
self.set_cache_usertoken(&n_tok)?;
|
||||
self.set_cache_userpassword(&n_tok.uuid, cred)?;
|
||||
Ok(Some(true))
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("incorrect password");
|
||||
// PW failed the check.
|
||||
Ok(Some(false))
|
||||
}
|
||||
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;
|
||||
token
|
||||
.as_ref()
|
||||
.map(|t| self.check_cache_userpassword(&t.uuid, cred))
|
||||
.transpose()
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Some(OperationError::NotAuthenticated),
|
||||
) => {
|
||||
error!("transport unauthenticated, moving to offline");
|
||||
// Something went wrong, mark offline.
|
||||
let time = SystemTime::now().add(Duration::from_secs(15));
|
||||
self.set_cachestate(CacheState::OfflineNextCheck(time))
|
||||
.await;
|
||||
token
|
||||
.as_ref()
|
||||
.map(|t| self.check_cache_userpassword(&t.uuid, cred))
|
||||
.transpose()
|
||||
}
|
||||
ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::NoMatchingEntries),
|
||||
)
|
||||
| ClientError::Http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Some(OperationError::InvalidAccountState(_)),
|
||||
) => {
|
||||
error!("unknown account or is not a valid posix account");
|
||||
Ok(None)
|
||||
}
|
||||
er => {
|
||||
error!("client error -> {:?}", er);
|
||||
// Some other unknown processing error?
|
||||
Err(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn offline_account_authenticate(
|
||||
&self,
|
||||
token: &Option<UnixUserToken>,
|
||||
cred: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
debug!("Attempt offline password check");
|
||||
token
|
||||
.as_ref()
|
||||
.map(|t| self.check_cache_userpassword(&t.uuid, cred))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn pam_account_allowed(&self, account_id: &str) -> Result<Option<bool>, ()> {
|
||||
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
||||
|
||||
Ok(token.map(|tok| {
|
||||
let user_set: BTreeSet<_> = tok.groups.iter().map(|g| g.name.clone()).collect();
|
||||
|
||||
debug!(
|
||||
"Checking if -> {:?} & {:?}",
|
||||
user_set, self.pam_allow_groups
|
||||
);
|
||||
|
||||
let b = user_set.intersection(&self.pam_allow_groups).count() > 0;
|
||||
b
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn pam_account_authenticate(
|
||||
&self,
|
||||
account_id: &str,
|
||||
cred: &str,
|
||||
) -> Result<Option<bool>, ()> {
|
||||
let state = self.get_cachestate().await;
|
||||
let (_expired, token) = self.get_cached_usertoken(&Id::Name(account_id.to_string()))?;
|
||||
|
||||
match state {
|
||||
CacheState::Online => {
|
||||
self.online_account_authenticate(&token, account_id, cred)
|
||||
.await
|
||||
}
|
||||
CacheState::OfflineNextCheck(time) => {
|
||||
// Should this always attempt to go online?
|
||||
if SystemTime::now() >= time && self.test_connection().await {
|
||||
// Brought ourselves online, lets check.
|
||||
self.online_account_authenticate(&token, account_id, cred)
|
||||
.await
|
||||
} else {
|
||||
// We are offline, check from the cache if possible.
|
||||
self.offline_account_authenticate(&token, cred)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// We are offline, check from the cache if possible.
|
||||
self.offline_account_authenticate(&token, cred)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_connection(&self) -> bool {
|
||||
let state = self.get_cachestate().await;
|
||||
match state {
|
||||
|
|
|
@ -7,7 +7,7 @@ use structopt::StructOpt;
|
|||
use futures::executor::block_on;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -30,14 +30,18 @@ async fn main() {
|
|||
|
||||
debug!("Starting cache invalidate tool ...");
|
||||
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
if !opt.really {
|
||||
error!("Are you sure you want to proceed? If so use --really");
|
||||
return;
|
||||
}
|
||||
|
||||
let req = ClientRequest::InvalidateCache;
|
||||
let req = ClientRequest::ClearCache;
|
||||
|
||||
match block_on(call_daemon(DEFAULT_SOCK_PATH, req)) {
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("success"),
|
||||
_ => {
|
||||
|
|
|
@ -7,7 +7,7 @@ use structopt::StructOpt;
|
|||
use futures::executor::block_on;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -27,9 +27,14 @@ async fn main() {
|
|||
env_logger::init();
|
||||
|
||||
debug!("Starting cache invalidate tool ...");
|
||||
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
let req = ClientRequest::InvalidateCache;
|
||||
|
||||
match block_on(call_daemon(DEFAULT_SOCK_PATH, req)) {
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("success"),
|
||||
_ => {
|
||||
|
|
|
@ -15,9 +15,7 @@ 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_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
//=== the codec
|
||||
|
@ -45,11 +43,11 @@ impl Encoder for ClientCodec {
|
|||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, msg: ClientResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
debug!("Attempting to send response -> {:?} ...", msg);
|
||||
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(())
|
||||
}
|
||||
|
@ -153,6 +151,22 @@ async fn handle_client(
|
|||
ClientResponse::NssGroup(None)
|
||||
})
|
||||
}
|
||||
ClientRequest::PamAuthenticate(account_id, cred) => {
|
||||
debug!("pam authenticate");
|
||||
cachelayer
|
||||
.pam_account_authenticate(account_id.as_str(), cred.as_str())
|
||||
.await
|
||||
.map(|r| ClientResponse::PamStatus(r))
|
||||
.unwrap_or(ClientResponse::Error)
|
||||
}
|
||||
ClientRequest::PamAccountAllowed(account_id) => {
|
||||
debug!("pam account allowed");
|
||||
cachelayer
|
||||
.pam_account_allowed(account_id.as_str())
|
||||
.await
|
||||
.map(|r| ClientResponse::PamStatus(r))
|
||||
.unwrap_or(ClientResponse::Error)
|
||||
}
|
||||
ClientRequest::InvalidateCache => {
|
||||
debug!("invalidate cache");
|
||||
cachelayer
|
||||
|
@ -190,29 +204,35 @@ async fn handle_client(
|
|||
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 cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
rm_if_exist(cfg.sock_path.as_str());
|
||||
|
||||
let cb = cb.connect_timeout(cfg.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,
|
||||
cfg.db_path.as_str(), // The sqlite db path
|
||||
cfg.cache_timeout,
|
||||
rsclient,
|
||||
cfg.pam_allowed_login_groups.clone(),
|
||||
)
|
||||
.expect("Failed to build cache layer."),
|
||||
);
|
||||
|
||||
// Set the umask while we open the path
|
||||
let before = unsafe { umask(0) };
|
||||
let mut listener = UnixListener::bind(DEFAULT_SOCK_PATH).unwrap();
|
||||
let mut listener = UnixListener::bind(cfg.sock_path.as_str()).unwrap();
|
||||
// Undo it.
|
||||
let _ = unsafe { umask(before) };
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use structopt::StructOpt;
|
|||
use futures::executor::block_on;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -27,9 +27,14 @@ async fn main() {
|
|||
env_logger::init();
|
||||
|
||||
debug!("Starting cache invalidate tool ...");
|
||||
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
let req = ClientRequest::Status;
|
||||
|
||||
match block_on(call_daemon(DEFAULT_SOCK_PATH, req)) {
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::Ok => info!("working!"),
|
||||
_ => {
|
||||
|
|
|
@ -8,6 +8,9 @@ use std::fmt;
|
|||
use crate::cache::Id;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use kanidm::be::dbvalue::DbPasswordV1;
|
||||
use kanidm::credential::Password;
|
||||
|
||||
pub struct Db {
|
||||
pool: Pool<SqliteConnectionManager>,
|
||||
lock: Mutex<()>,
|
||||
|
@ -137,7 +140,7 @@ impl<'a> DbTxn<'a> {
|
|||
.execute("COMMIT TRANSACTION", NO_PARAMS)
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
debug!("sqlite commit failure -> {:?}", e);
|
||||
error!("sqlite commit failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
}
|
||||
|
@ -146,14 +149,14 @@ impl<'a> DbTxn<'a> {
|
|||
self.conn
|
||||
.execute("UPDATE group_t SET expiry = 0", NO_PARAMS)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite update group_t failure -> {:?}", e);
|
||||
error!("sqlite update group_t failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
self.conn
|
||||
.execute("UPDATE account_t SET expiry = 0", NO_PARAMS)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite update account_t failure -> {:?}", e);
|
||||
error!("sqlite update account_t failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
|
@ -164,14 +167,14 @@ impl<'a> DbTxn<'a> {
|
|||
self.conn
|
||||
.execute("DELETE FROM group_t", NO_PARAMS)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite delete group_t failure -> {:?}", e);
|
||||
error!("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);
|
||||
error!("sqlite delete group_t failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
|
@ -292,7 +295,6 @@ impl<'a> DbTxn<'a> {
|
|||
data.iter()
|
||||
.map(|token| {
|
||||
// token convert with cbor.
|
||||
debug!("{:?}", token);
|
||||
serde_cbor::from_slice(token.as_slice()).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
|
@ -311,6 +313,23 @@ impl<'a> DbTxn<'a> {
|
|||
()
|
||||
})?;
|
||||
|
||||
// This isn't needed because insert or replace into seems to clean up dups!
|
||||
/*
|
||||
self.conn.execute_named("DELETE FROM account_t WHERE NOT uuid = :uuid AND (name = :name OR spn = :spn OR gidnumber = :gidnumber)",
|
||||
&[
|
||||
(":uuid", &account.uuid),
|
||||
(":name", &account.name),
|
||||
(":spn", &account.spn),
|
||||
(":gidnumber", &account.gidnumber),
|
||||
]
|
||||
)
|
||||
.map_err(|e| {
|
||||
debug!("sqlite delete account_t duplicate failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
.map(|_| ())
|
||||
*/
|
||||
|
||||
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| {
|
||||
|
@ -386,6 +405,78 @@ impl<'a> DbTxn<'a> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), ()> {
|
||||
let pw = Password::new(cred);
|
||||
let dbpw = pw.to_dbpasswordv1();
|
||||
let data = serde_cbor::to_vec(&dbpw).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
self.conn
|
||||
.execute_named(
|
||||
"UPDATE account_t SET password = :data WHERE uuid = :a_uuid",
|
||||
&[(":a_uuid", &a_uuid), (":data", &data)],
|
||||
)
|
||||
.map_err(|e| {
|
||||
error!("sqlite update account_t password failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn check_account_password(&self, a_uuid: &str, cred: &str) -> Result<bool, ()> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT password FROM account_t WHERE uuid = :a_uuid AND password IS NOT NULL")
|
||||
.map_err(|e| {
|
||||
error!("sqlite select prepare failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
|
||||
// Makes tuple (token, expiry)
|
||||
let data_iter = stmt
|
||||
.query_map(&[a_uuid], |row| Ok(row.get(0)?))
|
||||
.map_err(|e| {
|
||||
error!("sqlite query_map failure -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
let data: Result<Vec<Vec<u8>>, _> = data_iter
|
||||
.map(|v| {
|
||||
v.map_err(|e| {
|
||||
error!("sqlite map failure -> {:?}", e);
|
||||
()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let data = data?;
|
||||
|
||||
if data.len() == 0 {
|
||||
info!("No cached password, failing authentication");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if data.len() >= 2 {
|
||||
error!("invalid db state, multiple entries matched query?");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let r: Result<bool, ()> = data
|
||||
.first()
|
||||
.map(|raw| {
|
||||
// Map the option from data.first.
|
||||
let dbpw: DbPasswordV1 = serde_cbor::from_slice(raw.as_slice()).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
})?;
|
||||
let pw = Password::try_from(dbpw)?;
|
||||
Ok(pw.verify(cred))
|
||||
})
|
||||
.unwrap_or(Ok(false));
|
||||
r
|
||||
}
|
||||
|
||||
fn get_group_data_name(&self, grp_id: &str) -> Result<Vec<(Vec<u8>, i64)>, ()> {
|
||||
let mut stmt = self.conn
|
||||
.prepare(
|
||||
|
@ -500,7 +591,7 @@ impl<'a> DbTxn<'a> {
|
|||
data.iter()
|
||||
.map(|token| {
|
||||
// token convert with cbor.
|
||||
debug!("{:?}", token);
|
||||
// debug!("{:?}", token);
|
||||
serde_cbor::from_slice(token.as_slice()).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
|
@ -538,7 +629,7 @@ impl<'a> DbTxn<'a> {
|
|||
data.iter()
|
||||
.map(|token| {
|
||||
// token convert with cbor.
|
||||
debug!("{:?}", token);
|
||||
// debug!("{:?}", token);
|
||||
serde_cbor::from_slice(token.as_slice()).map_err(|e| {
|
||||
error!("cbor error -> {:?}", e);
|
||||
()
|
||||
|
@ -617,6 +708,9 @@ mod tests {
|
|||
use crate::cache::Id;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
|
||||
static TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
|
||||
static TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_basic() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
@ -841,4 +935,165 @@ mod tests {
|
|||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_password() {
|
||||
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 uuid1 = "0302b99c-f0f6-41ab-9492-852692b0fd16";
|
||||
let 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 that with no account, is false
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false));
|
||||
// test adding an account
|
||||
dbtxn.update_account(&ut1, 0).unwrap();
|
||||
// check with no password is false.
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false));
|
||||
// update the pw
|
||||
assert!(dbtxn
|
||||
.update_account_password(uuid1, TESTACCOUNT1_PASSWORD_A)
|
||||
.is_ok());
|
||||
// Check it now works.
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(true));
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) == Ok(false));
|
||||
// Update the pw
|
||||
assert!(dbtxn
|
||||
.update_account_password(uuid1, TESTACCOUNT1_PASSWORD_B)
|
||||
.is_ok());
|
||||
// Check it matches.
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false));
|
||||
assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) == Ok(true));
|
||||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_group_rename_duplicate() {
|
||||
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 gt1 = UnixGroupToken {
|
||||
name: "testgroup".to_string(),
|
||||
spn: "testgroup@example.com".to_string(),
|
||||
gidnumber: 2000,
|
||||
uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(),
|
||||
};
|
||||
|
||||
let gt2 = UnixGroupToken {
|
||||
name: "testgroup".to_string(),
|
||||
spn: "testgroup@example.com".to_string(),
|
||||
gidnumber: 2001,
|
||||
uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(),
|
||||
};
|
||||
|
||||
let id_name = Id::Name("testgroup".to_string());
|
||||
let id_name2 = Id::Name("testgroup2".to_string());
|
||||
|
||||
// test finding no group
|
||||
let r1 = dbtxn.get_group(&id_name).unwrap();
|
||||
assert!(r1.is_none());
|
||||
|
||||
// test adding a group
|
||||
dbtxn.update_group(>1, 0).unwrap();
|
||||
let r0 = dbtxn.get_group(&id_name).unwrap();
|
||||
assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16");
|
||||
|
||||
// Do the "rename" of gt1 which is what would allow gt2 to be valid.
|
||||
gt1.name = "testgroup2".to_string();
|
||||
gt1.spn = "testgroup2@example.com".to_string();
|
||||
// Now, add gt2 which dups on gt1 name/spn.
|
||||
dbtxn.update_group(>2, 0).unwrap();
|
||||
let r2 = dbtxn.get_group(&id_name).unwrap();
|
||||
assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b");
|
||||
let r3 = dbtxn.get_group(&id_name2).unwrap();
|
||||
assert!(r3.is_none());
|
||||
|
||||
// Now finally update gt1
|
||||
dbtxn.update_group(>1, 0).unwrap();
|
||||
|
||||
// Both now coexist
|
||||
let r4 = dbtxn.get_group(&id_name).unwrap();
|
||||
assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b");
|
||||
let r5 = dbtxn.get_group(&id_name2).unwrap();
|
||||
assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16");
|
||||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_db_account_rename_duplicate() {
|
||||
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()],
|
||||
};
|
||||
|
||||
let ut2 = UnixUserToken {
|
||||
name: "testuser".to_string(),
|
||||
spn: "testuser@example.com".to_string(),
|
||||
displayname: "Test User".to_string(),
|
||||
gidnumber: 2001,
|
||||
uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(),
|
||||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
};
|
||||
|
||||
let id_name = Id::Name("testuser".to_string());
|
||||
let id_name2 = Id::Name("testuser2".to_string());
|
||||
|
||||
// test finding no account
|
||||
let r1 = dbtxn.get_account(&id_name).unwrap();
|
||||
assert!(r1.is_none());
|
||||
|
||||
// test adding an account
|
||||
dbtxn.update_account(&ut1, 0).unwrap();
|
||||
let r0 = dbtxn.get_account(&id_name).unwrap();
|
||||
assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16");
|
||||
|
||||
// Do the "rename" of gt1 which is what would allow gt2 to be valid.
|
||||
ut1.name = "testuser2".to_string();
|
||||
ut1.spn = "testuser2@example.com".to_string();
|
||||
// Now, add gt2 which dups on gt1 name/spn.
|
||||
dbtxn.update_account(&ut2, 0).unwrap();
|
||||
let r2 = dbtxn.get_account(&id_name).unwrap();
|
||||
assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b");
|
||||
let r3 = dbtxn.get_account(&id_name2).unwrap();
|
||||
assert!(r3.is_none());
|
||||
|
||||
// Now finally update gt1
|
||||
dbtxn.update_account(&ut1, 0).unwrap();
|
||||
|
||||
// Both now coexist
|
||||
let r4 = dbtxn.get_account(&id_name).unwrap();
|
||||
assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b");
|
||||
let r5 = dbtxn.get_account(&id_name2).unwrap();
|
||||
assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16");
|
||||
|
||||
assert!(dbtxn.commit().is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ extern crate log;
|
|||
|
||||
pub mod cache;
|
||||
pub mod client;
|
||||
pub mod constants;
|
||||
mod constants;
|
||||
pub(crate) mod db;
|
||||
pub mod unix_config;
|
||||
pub mod unix_proto;
|
||||
|
|
|
@ -7,7 +7,7 @@ use structopt::StructOpt;
|
|||
use futures::executor::block_on;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::constants::DEFAULT_SOCK_PATH;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -29,9 +29,14 @@ async fn main() {
|
|||
env_logger::init();
|
||||
|
||||
debug!("Starting authorized keys tool ...");
|
||||
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
let req = ClientRequest::SshKey(opt.account_id.clone());
|
||||
|
||||
match block_on(call_daemon(DEFAULT_SOCK_PATH, req)) {
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::SshKeys(sk) => sk.iter().for_each(|k| {
|
||||
println!("{}", k);
|
||||
|
|
83
kanidm_unix_int/src/test_auth.rs
Normal file
83
kanidm_unix_int/src/test_auth.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use log::debug;
|
||||
use structopt::StructOpt;
|
||||
|
||||
use futures::executor::block_on;
|
||||
|
||||
use kanidm_unix_common::client::call_daemon;
|
||||
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
||||
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct ClientOpt {
|
||||
#[structopt(short = "d", long = "debug")]
|
||||
debug: bool,
|
||||
#[structopt(short = "D", long = "name")]
|
||||
account_id: String,
|
||||
}
|
||||
|
||||
#[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 cache invalidate tool ...");
|
||||
|
||||
let cfg = KanidmUnixdConfig::new()
|
||||
.read_options_from_optional_config("/etc/kanidm/unixd")
|
||||
.expect("Failed to parse /etc/kanidm/unixd");
|
||||
|
||||
let password = rpassword::prompt_password_stderr("Enter unix password: ").unwrap();
|
||||
|
||||
let req = ClientRequest::PamAuthenticate(opt.account_id.clone(), password);
|
||||
let sereq = ClientRequest::PamAccountAllowed(opt.account_id.clone());
|
||||
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), req)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
info!("auth success!");
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
info!("auth failed!");
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
info!("user unknown");
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match block_on(call_daemon(cfg.sock_path.as_str(), sereq)) {
|
||||
Ok(r) => match r {
|
||||
ClientResponse::PamStatus(Some(true)) => {
|
||||
info!("auth success!");
|
||||
}
|
||||
ClientResponse::PamStatus(Some(false)) => {
|
||||
info!("auth failed!");
|
||||
}
|
||||
ClientResponse::PamStatus(None) => {
|
||||
info!("user unknown");
|
||||
}
|
||||
_ => {
|
||||
// unexpected response.
|
||||
error!("Error: unexpected response -> {:?}", r);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
68
kanidm_unix_int/src/unix_config.rs
Normal file
68
kanidm_unix_int/src/unix_config.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use crate::constants::{
|
||||
DEFAULT_CACHE_TIMEOUT, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_SOCK_PATH,
|
||||
};
|
||||
use serde_derive::Deserialize;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConfigInt {
|
||||
db_path: Option<String>,
|
||||
sock_path: Option<String>,
|
||||
conn_timeout: Option<u64>,
|
||||
cache_timeout: Option<u64>,
|
||||
pam_allowed_login_groups: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KanidmUnixdConfig {
|
||||
pub db_path: String,
|
||||
pub sock_path: String,
|
||||
pub conn_timeout: u64,
|
||||
pub cache_timeout: u64,
|
||||
pub pam_allowed_login_groups: Vec<String>,
|
||||
}
|
||||
|
||||
impl KanidmUnixdConfig {
|
||||
pub fn new() -> Self {
|
||||
KanidmUnixdConfig {
|
||||
db_path: DEFAULT_DB_PATH.to_string(),
|
||||
sock_path: DEFAULT_SOCK_PATH.to_string(),
|
||||
conn_timeout: DEFAULT_CONN_TIMEOUT,
|
||||
cache_timeout: DEFAULT_CACHE_TIMEOUT,
|
||||
pam_allowed_login_groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_options_from_optional_config<P: AsRef<Path>>(
|
||||
self,
|
||||
config_path: P,
|
||||
) -> Result<Self, ()> {
|
||||
let mut f = match File::open(config_path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!("Unabled to open config file [{:?}], skipping ...", e);
|
||||
return Ok(self);
|
||||
}
|
||||
};
|
||||
|
||||
let mut contents = String::new();
|
||||
f.read_to_string(&mut contents)
|
||||
.map_err(|e| eprintln!("{:?}", e))?;
|
||||
|
||||
let config: ConfigInt =
|
||||
toml::from_str(contents.as_str()).map_err(|e| eprintln!("{:?}", e))?;
|
||||
|
||||
// Now map the values into our config.
|
||||
Ok(KanidmUnixdConfig {
|
||||
db_path: config.db_path.unwrap_or(self.db_path),
|
||||
sock_path: config.sock_path.unwrap_or(self.sock_path),
|
||||
conn_timeout: config.conn_timeout.unwrap_or(self.conn_timeout),
|
||||
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
|
||||
pam_allowed_login_groups: config
|
||||
.pam_allowed_login_groups
|
||||
.unwrap_or(self.pam_allowed_login_groups),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ pub enum ClientRequest {
|
|||
NssGroups,
|
||||
NssGroupByGid(u32),
|
||||
NssGroupByName(String),
|
||||
PamAuthenticate(String, String),
|
||||
PamAccountAllowed(String),
|
||||
InvalidateCache,
|
||||
ClearCache,
|
||||
Status,
|
||||
|
@ -35,6 +37,7 @@ pub enum ClientResponse {
|
|||
NssAccount(Option<NssUser>),
|
||||
NssGroups(Vec<NssGroup>),
|
||||
NssGroup(Option<NssGroup>),
|
||||
PamStatus(Option<bool>),
|
||||
Ok,
|
||||
Error,
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ use kanidm_client::{KanidmClient, KanidmClientBuilder};
|
|||
|
||||
static PORT_ALLOC: AtomicUsize = AtomicUsize::new(18080);
|
||||
static ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
||||
static TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
|
||||
static TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
|
||||
static TESTACCOUNT1_PASSWORD_INC: &str = "never going to work";
|
||||
|
||||
fn run_test(fix_fn: fn(&KanidmClient) -> (), test_fn: fn(CacheLayer, KanidmAsyncClient) -> ()) {
|
||||
// ::std::env::set_var("RUST_LOG", "actix_web=debug,kanidm=debug");
|
||||
|
@ -64,7 +67,9 @@ fn run_test(fix_fn: fn(&KanidmClient) -> (), test_fn: fn(CacheLayer, KanidmAsync
|
|||
|
||||
let cachelayer = CacheLayer::new(
|
||||
"", // The sqlite db path, this is in memory.
|
||||
300, rsclient,
|
||||
300,
|
||||
rsclient,
|
||||
vec!["allowed_group".to_string()],
|
||||
)
|
||||
.expect("Failed to build cache layer.");
|
||||
|
||||
|
@ -97,6 +102,10 @@ fn test_fixture(rsclient: &KanidmClient) -> () {
|
|||
.idm_account_post_ssh_pubkey("testaccount1", "tk",
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAeGW1P6Pc2rPq0XqbRaDKBcXZUPRklo0L1EyR30CwoP william@amethyst")
|
||||
.unwrap();
|
||||
// Set a posix password
|
||||
rsclient
|
||||
.idm_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.unwrap();
|
||||
|
||||
// Setup a group
|
||||
rsclient.idm_group_create("testgroup1").unwrap();
|
||||
|
@ -106,6 +115,12 @@ fn test_fixture(rsclient: &KanidmClient) -> () {
|
|||
rsclient
|
||||
.idm_group_unix_extend("testgroup1", Some(20001))
|
||||
.unwrap();
|
||||
|
||||
// Setup the allowed group
|
||||
rsclient.idm_group_create("allowed_group").unwrap();
|
||||
rsclient
|
||||
.idm_group_unix_extend("allowed_group", Some(20002))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -341,3 +356,163 @@ fn test_cache_account_delete() {
|
|||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_account_password() {
|
||||
run_test(test_fixture, |cachelayer, adminclient| {
|
||||
let mut rt = Runtime::new().expect("Failed to start tokio");
|
||||
let fut = async move {
|
||||
cachelayer.attempt_online().await;
|
||||
// Test authentication failure.
|
||||
let a1 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a1 == Some(false));
|
||||
|
||||
// Test authentication success.
|
||||
let a2 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a2 == Some(true));
|
||||
|
||||
// change pw
|
||||
adminclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await
|
||||
.expect("failed to auth as admin");
|
||||
adminclient
|
||||
.idm_account_unix_cred_put("testaccount1", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("Failed to change password");
|
||||
|
||||
// test auth (old pw) fail
|
||||
let a3 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a3 == Some(false));
|
||||
|
||||
// test auth (new pw) success
|
||||
let a4 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a4 == Some(true));
|
||||
|
||||
// Go offline.
|
||||
cachelayer.mark_offline().await;
|
||||
|
||||
// Test auth success
|
||||
let a5 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a5 == Some(true));
|
||||
|
||||
// Test auth failure.
|
||||
let a6 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_INC)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a6 == Some(false));
|
||||
|
||||
// clear cache
|
||||
cachelayer.clear_cache().expect("failed to clear cache");
|
||||
|
||||
// test auth good (fail)
|
||||
let a7 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a7 == None);
|
||||
|
||||
// go online
|
||||
cachelayer.attempt_online().await;
|
||||
assert!(cachelayer.test_connection().await);
|
||||
|
||||
// test auth success
|
||||
let a8 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a8 == Some(true));
|
||||
};
|
||||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_account_pam_allowed() {
|
||||
run_test(test_fixture, |cachelayer, adminclient| {
|
||||
let mut rt = Runtime::new().expect("Failed to start tokio");
|
||||
let fut = async move {
|
||||
cachelayer.attempt_online().await;
|
||||
|
||||
// Should fail
|
||||
let a1 = cachelayer
|
||||
.pam_account_allowed("testaccount1")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a1 == Some(false));
|
||||
|
||||
adminclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await
|
||||
.expect("failed to auth as admin");
|
||||
adminclient
|
||||
.idm_group_add_members("allowed_group", vec!["testaccount1"])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Invalidate cache to force a refresh
|
||||
assert!(cachelayer.invalidate().is_ok());
|
||||
|
||||
// Should pass
|
||||
let a2 = cachelayer
|
||||
.pam_account_allowed("testaccount1")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a2 == Some(true));
|
||||
};
|
||||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_account_pam_nonexist() {
|
||||
run_test(test_fixture, |cachelayer, _adminclient| {
|
||||
let mut rt = Runtime::new().expect("Failed to start tokio");
|
||||
let fut = async move {
|
||||
cachelayer.attempt_online().await;
|
||||
|
||||
let a1 = cachelayer
|
||||
.pam_account_allowed("NO_SUCH_ACCOUNT")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a1 == None);
|
||||
|
||||
let a2 = cachelayer
|
||||
.pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a2 == None);
|
||||
|
||||
cachelayer.mark_offline().await;
|
||||
|
||||
let a1 = cachelayer
|
||||
.pam_account_allowed("NO_SUCH_ACCOUNT")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a1 == None);
|
||||
|
||||
let a2 = cachelayer
|
||||
.pam_account_authenticate("NO_SUCH_ACCOUNT", TESTACCOUNT1_PASSWORD_B)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a2 == None);
|
||||
};
|
||||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ RUN zypper mr -d repo-non-oss && \
|
|||
zypper ar https://download.opensuse.org/update/tumbleweed/ repo-update-https && \
|
||||
zypper ar https://download.opensuse.org/tumbleweed/repo/oss/ repo-oss-https && \
|
||||
zypper ar https://download.opensuse.org/tumbleweed/repo/non-oss/ repo-non-oss-https && \
|
||||
zypper install -y timezone cargo rust gcc sqlite3-devel libopenssl-devel
|
||||
zypper install -y timezone cargo rust gcc sqlite3-devel libopenssl-devel pam-devel
|
||||
COPY . /home/kanidm/
|
||||
WORKDIR /home/kanidm/
|
||||
RUN cargo build --release
|
||||
|
|
|
@ -4,7 +4,9 @@ use crate::audit::AuditScope;
|
|||
|
||||
use crate::async_log::EventLog;
|
||||
use crate::event::{AuthEvent, SearchEvent, SearchResult, WhoamiResult};
|
||||
use crate::idm::event::{RadiusAuthTokenEvent, UnixGroupTokenEvent, UnixUserTokenEvent};
|
||||
use crate::idm::event::{
|
||||
RadiusAuthTokenEvent, UnixGroupTokenEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::value::PartialValue;
|
||||
use kanidm_proto::v1::{OperationError, RadiusAuthToken};
|
||||
|
||||
|
@ -139,6 +141,16 @@ impl Message for InternalSshKeyTagReadMessage {
|
|||
type Result = Result<Option<String>, OperationError>;
|
||||
}
|
||||
|
||||
pub struct IdmAccountUnixAuthMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
pub cred: String,
|
||||
}
|
||||
|
||||
impl Message for IdmAccountUnixAuthMessage {
|
||||
type Result = Result<Option<UnixUserToken>, OperationError>;
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
|
||||
pub struct QueryServerReadV1 {
|
||||
|
@ -653,3 +665,54 @@ impl Handler<InternalSshKeyTagReadMessage> for QueryServerReadV1 {
|
|||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<IdmAccountUnixAuthMessage> for QueryServerReadV1 {
|
||||
type Result = Result<Option<UnixUserToken>, OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: IdmAccountUnixAuthMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("idm_account_unix_auth");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let mut idm_write = self.idms.write();
|
||||
|
||||
// resolve the id
|
||||
let target_uuid = Uuid::parse_str(msg.uuid_or_name.as_str()).or_else(|_| {
|
||||
idm_write
|
||||
.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 uuae = match UnixUserAuthEvent::from_parts(
|
||||
&mut audit,
|
||||
&idm_write.qs_read,
|
||||
msg.uat,
|
||||
target_uuid,
|
||||
msg.cred,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
audit_log!(audit, "Failed to begin unix auth: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
audit_log!(audit, "Begin event {:?}", uuae);
|
||||
|
||||
let ct = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Clock failure!");
|
||||
|
||||
let r = idm_write
|
||||
.auth_unix(&mut audit, &uuae, ct)
|
||||
.and_then(|r| idm_write.commit().map(|_| r));
|
||||
|
||||
audit_log!(audit, "Sending result -> {:?}", r);
|
||||
r
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ use crate::async_log::EventLog;
|
|||
use crate::event::{
|
||||
CreateEvent, DeleteEvent, ModifyEvent, PurgeRecycledEvent, PurgeTombstoneEvent,
|
||||
};
|
||||
use crate::idm::event::{GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent};
|
||||
use crate::idm::event::{
|
||||
GeneratePasswordEvent, PasswordChangeEvent, RegenerateRadiusSecretEvent,
|
||||
UnixPasswordChangeEvent,
|
||||
};
|
||||
use crate::modify::{Modify, ModifyInvalid, ModifyList};
|
||||
use crate::value::{PartialValue, Value};
|
||||
use kanidm_proto::v1::OperationError;
|
||||
|
@ -148,6 +151,16 @@ impl Message for IdmGroupUnixExtendMessage {
|
|||
type Result = Result<(), OperationError>;
|
||||
}
|
||||
|
||||
pub struct IdmAccountUnixSetCredMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
pub cred: String,
|
||||
}
|
||||
|
||||
impl Message for IdmAccountUnixSetCredMessage {
|
||||
type Result = Result<(), OperationError>;
|
||||
}
|
||||
|
||||
pub struct InternalCredentialSetMessage {
|
||||
pub uat: Option<UserAuthToken>,
|
||||
pub uuid_or_name: String,
|
||||
|
@ -869,6 +882,45 @@ impl Handler<IdmGroupUnixExtendMessage> for QueryServerWriteV1 {
|
|||
}
|
||||
}
|
||||
|
||||
impl Handler<IdmAccountUnixSetCredMessage> for QueryServerWriteV1 {
|
||||
type Result = Result<(), OperationError>;
|
||||
|
||||
fn handle(&mut self, msg: IdmAccountUnixSetCredMessage, _: &mut Self::Context) -> Self::Result {
|
||||
let mut audit = AuditScope::new("idm_account_unix_set_cred");
|
||||
let res = audit_segment!(&mut audit, || {
|
||||
let mut idms_prox_write = self.idms.proxy_write();
|
||||
|
||||
let target_uuid = Uuid::parse_str(msg.uuid_or_name.as_str()).or_else(|_| {
|
||||
idms_prox_write
|
||||
.qs_write
|
||||
.posixid_to_uuid(&mut audit, msg.uuid_or_name.as_str())
|
||||
.map_err(|e| {
|
||||
audit_log!(&mut audit, "Error resolving as gidnumber continuing ...");
|
||||
e
|
||||
})
|
||||
})?;
|
||||
|
||||
let upce = UnixPasswordChangeEvent::from_parts(
|
||||
&mut audit,
|
||||
&idms_prox_write.qs_write,
|
||||
msg.uat,
|
||||
target_uuid,
|
||||
msg.cred,
|
||||
)
|
||||
.map_err(|e| {
|
||||
audit_log!(audit, "Failed to begin UnixPasswordChangeEvent: {:?}", e);
|
||||
e
|
||||
})?;
|
||||
idms_prox_write
|
||||
.set_unix_account_password(&mut audit, &upce)
|
||||
.and_then(|_| idms_prox_write.commit(&mut audit))
|
||||
.map(|_| ())
|
||||
});
|
||||
self.log.do_send(audit);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
// These below are internal only types.
|
||||
|
||||
impl Handler<PurgeTombstoneEvent> for QueryServerWriteV1 {
|
||||
|
|
|
@ -5,7 +5,7 @@ pub mod system_config;
|
|||
pub use crate::constants::system_config::JSON_SYSTEM_CONFIG_V1;
|
||||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub static SYSTEM_INDEX_VERSION: i64 = 4;
|
||||
pub static SYSTEM_INDEX_VERSION: i64 = 5;
|
||||
// On test builds, define to 60 seconds
|
||||
#[cfg(test)]
|
||||
pub static PURGE_TIMEOUT: u64 = 60;
|
||||
|
@ -109,6 +109,7 @@ pub static UUID_SCHEMA_CLASS_POSIXGROUP: &str = "00000000-0000-0000-0000-ffff000
|
|||
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";
|
||||
pub static UUID_SCHEMA_ATTR_UNIX_PASSWORD: &str = "00000000-0000-0000-0000-ffff00000062";
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for fucking up these allocations.
|
||||
|
@ -591,10 +592,10 @@ pub static JSON_IDM_SELF_ACP_WRITE_V1: &str = r#"{
|
|||
"\"Self\""
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey"
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey"
|
||||
"name", "displayname", "legalname", "radius_secret", "primary_credential", "ssh_publickey", "unix_password"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -1301,13 +1302,13 @@ pub static JSON_IDM_ACP_ACCOUNT_UNIX_EXTEND_PRIV_V1: &str = r#"{
|
|||
"{\"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"
|
||||
"class", "name", "spn", "uuid", "description", "gidnumber", "loginshell", "unix_password"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"class", "loginshell", "gidnumber"
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"class", "loginshell", "gidnumber"
|
||||
"class", "loginshell", "gidnumber", "unix_password"
|
||||
],
|
||||
"acp_modify_class": ["posixaccount"]
|
||||
}
|
||||
|
@ -1733,6 +1734,35 @@ pub static JSON_SCHEMA_ATTR_LOGINSHELL: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub static JSON_SCHEMA_ATTR_UNIX_PASSWORD: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"A posix users unix login password."
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"unix_password"
|
||||
],
|
||||
"syntax": [
|
||||
"CREDENTIAL"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000062"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub static JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
{
|
||||
"valid": {
|
||||
|
@ -1901,7 +1931,8 @@ pub static JSON_SCHEMA_CLASS_POSIXACCOUNT: &str = r#"
|
|||
"posixaccount"
|
||||
],
|
||||
"systemmay": [
|
||||
"loginshell"
|
||||
"loginshell",
|
||||
"unix_password"
|
||||
],
|
||||
"systemmust": [
|
||||
"gidnumber"
|
||||
|
|
|
@ -16,16 +16,18 @@ use crate::config::Configuration;
|
|||
// SearchResult
|
||||
use crate::actors::v1_read::QueryServerReadV1;
|
||||
use crate::actors::v1_read::{
|
||||
AuthMessage, InternalRadiusReadMessage, InternalRadiusTokenReadMessage, InternalSearchMessage,
|
||||
InternalSshKeyReadMessage, InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage,
|
||||
AuthMessage, IdmAccountUnixAuthMessage, InternalRadiusReadMessage,
|
||||
InternalRadiusTokenReadMessage, InternalSearchMessage, InternalSshKeyReadMessage,
|
||||
InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage,
|
||||
InternalUnixUserTokenReadMessage, SearchMessage, WhoamiMessage,
|
||||
};
|
||||
use crate::actors::v1_write::QueryServerWriteV1;
|
||||
use crate::actors::v1_write::{
|
||||
AppendAttributeMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage,
|
||||
IdmAccountUnixExtendMessage, IdmGroupUnixExtendMessage, InternalCredentialSetMessage,
|
||||
InternalDeleteMessage, InternalRegenerateRadiusMessage, InternalSshKeyCreateMessage,
|
||||
ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage, SetAttributeMessage,
|
||||
IdmAccountUnixExtendMessage, IdmAccountUnixSetCredMessage, IdmGroupUnixExtendMessage,
|
||||
InternalCredentialSetMessage, InternalDeleteMessage, InternalRegenerateRadiusMessage,
|
||||
InternalSshKeyCreateMessage, ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage,
|
||||
SetAttributeMessage,
|
||||
};
|
||||
use crate::async_log;
|
||||
use crate::audit::AuditScope;
|
||||
|
@ -954,6 +956,119 @@ fn account_get_id_unix_token(
|
|||
Box::new(res)
|
||||
}
|
||||
|
||||
fn account_post_id_unix_auth(
|
||||
(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::<SingleStringRequest>(&body);
|
||||
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
let m_obj = IdmAccountUnixAuthMessage {
|
||||
uat: uat,
|
||||
uuid_or_name: id,
|
||||
cred: obj.value,
|
||||
};
|
||||
let res = state.qe_r.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_put_id_unix_credential(
|
||||
path: Path<String>,
|
||||
req: HttpRequest<AppState>,
|
||||
state: 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)
|
||||
}
|
||||
})
|
||||
.and_then(
|
||||
move |body| -> Box<dyn Future<Item = HttpResponse, Error = Error>> {
|
||||
let r_obj = serde_json::from_slice::<SingleStringRequest>(&body);
|
||||
match r_obj {
|
||||
Ok(obj) => {
|
||||
let m_obj = IdmAccountUnixSetCredMessage {
|
||||
uat,
|
||||
uuid_or_name: id,
|
||||
cred: obj.value,
|
||||
};
|
||||
let res = state.qe_w.send(m_obj).from_err().and_then(|res| match res {
|
||||
Ok(_) => Ok(HttpResponse::Ok().json(())),
|
||||
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_delete_id_unix_credential(
|
||||
(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 = PurgeAttributeMessage {
|
||||
uat,
|
||||
uuid_or_name: id,
|
||||
attr: "unix_password".to_string(),
|
||||
filter: filter_all!(f_eq("class", PartialValue::new_class("posixaccount"))),
|
||||
};
|
||||
|
||||
let res = state.qe_w.send(obj).from_err().and_then(|res| match res {
|
||||
Ok(()) => Ok(HttpResponse::Ok().json(())),
|
||||
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> {
|
||||
|
@ -1875,6 +1990,16 @@ pub fn create_server_core(config: Configuration) {
|
|||
r.method(http::Method::GET)
|
||||
.with_async(account_get_id_unix_token)
|
||||
})
|
||||
.resource("/v1/account/{id}/_unix/_auth", |r| {
|
||||
r.method(http::Method::POST)
|
||||
.with_async(account_post_id_unix_auth)
|
||||
})
|
||||
.resource("/v1/account/{id}/_unix/_credential", |r| {
|
||||
r.method(http::Method::PUT)
|
||||
.with_async(account_put_id_unix_credential);
|
||||
r.method(http::Method::DELETE)
|
||||
.with_async(account_delete_id_unix_credential);
|
||||
})
|
||||
// People
|
||||
// Groups
|
||||
.resource("/v1/group", |r| {
|
||||
|
|
|
@ -92,6 +92,14 @@ impl Password {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_dbpasswordv1(&self) -> DbPasswordV1 {
|
||||
match &self.material {
|
||||
KDF::PBKDF2(cost, salt, hash) => {
|
||||
DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -173,11 +181,7 @@ impl Credential {
|
|||
pub fn to_db_valuev1(&self) -> DbCredV1 {
|
||||
DbCredV1 {
|
||||
password: match &self.password {
|
||||
Some(pw) => match &pw.material {
|
||||
KDF::PBKDF2(cost, salt, hash) => {
|
||||
Some(DbPasswordV1::PBKDF2(*cost, salt.clone(), hash.clone()))
|
||||
}
|
||||
},
|
||||
Some(pw) => Some(pw.to_dbpasswordv1()),
|
||||
None => None,
|
||||
},
|
||||
claims: self.claims.clone(),
|
||||
|
|
|
@ -6,11 +6,13 @@
|
|||
//! with no ordering. An entry has many avas. A pseudo example, minus schema and typing:
|
||||
//!
|
||||
//! ```
|
||||
//! /*
|
||||
//! Entry {
|
||||
//! "name": ["william"],
|
||||
//! "uuid": ["..."],
|
||||
//! "mail": ["maila@example.com", "mailb@example.com"],
|
||||
//! }
|
||||
//! */
|
||||
//! ```
|
||||
//!
|
||||
//! There are three rules for entries:
|
||||
|
|
|
@ -60,6 +60,40 @@ impl PasswordChangeEvent {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnixPasswordChangeEvent {
|
||||
pub event: Event,
|
||||
pub target: Uuid,
|
||||
pub cleartext: String,
|
||||
}
|
||||
|
||||
impl UnixPasswordChangeEvent {
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: &Uuid, cleartext: &str) -> Self {
|
||||
UnixPasswordChangeEvent {
|
||||
event: Event::from_internal(),
|
||||
target: *target,
|
||||
cleartext: cleartext.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
uat: Option<UserAuthToken>,
|
||||
target: Uuid,
|
||||
cleartext: String,
|
||||
) -> Result<Self, OperationError> {
|
||||
let e = Event::from_rw_uat(audit, qs, uat)?;
|
||||
|
||||
Ok(UnixPasswordChangeEvent {
|
||||
event: e,
|
||||
target,
|
||||
cleartext,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GeneratePasswordEvent {
|
||||
pub event: Event,
|
||||
|
@ -188,3 +222,37 @@ impl UnixGroupTokenEvent {
|
|||
UnixGroupTokenEvent { event: e, target }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UnixUserAuthEvent {
|
||||
pub event: Event,
|
||||
pub target: Uuid,
|
||||
pub cleartext: String,
|
||||
}
|
||||
|
||||
impl UnixUserAuthEvent {
|
||||
#[cfg(test)]
|
||||
pub fn new_internal(target: &Uuid, cleartext: &str) -> Self {
|
||||
UnixUserAuthEvent {
|
||||
event: Event::from_internal(),
|
||||
target: *target,
|
||||
cleartext: cleartext.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parts(
|
||||
audit: &mut AuditScope,
|
||||
qs: &QueryServerReadTransaction,
|
||||
uat: Option<UserAuthToken>,
|
||||
target: Uuid,
|
||||
cleartext: String,
|
||||
) -> Result<Self, OperationError> {
|
||||
let e = Event::from_ro_uat(audit, qs, uat)?;
|
||||
|
||||
Ok(UnixUserAuthEvent {
|
||||
event: e,
|
||||
target,
|
||||
cleartext,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::idm::account::Account;
|
|||
use crate::idm::authsession::AuthSession;
|
||||
use crate::idm::event::{
|
||||
GeneratePasswordEvent, PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
UnixGroupTokenEvent, UnixUserTokenEvent,
|
||||
UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::idm::radius::RadiusAccount;
|
||||
use crate::idm::unix::{UnixGroup, UnixUserAccount};
|
||||
|
@ -43,7 +43,8 @@ pub struct IdmServerWriteTransaction<'a> {
|
|||
// the idm in memory structures (maybe the query server too). This is
|
||||
// things like authentication
|
||||
sessions: BptreeMapWriteTxn<'a, Uuid, AuthSession>,
|
||||
qs: &'a QueryServer,
|
||||
pub qs_read: QueryServerReadTransaction,
|
||||
// qs: &'a QueryServer,
|
||||
sid: &'a SID,
|
||||
}
|
||||
|
||||
|
@ -72,7 +73,8 @@ impl IdmServer {
|
|||
pub fn write(&self) -> IdmServerWriteTransaction {
|
||||
IdmServerWriteTransaction {
|
||||
sessions: self.sessions.write(),
|
||||
qs: &self.qs,
|
||||
// qs: &self.qs,
|
||||
qs_read: self.qs.read(),
|
||||
sid: &self.sid,
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +134,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
//
|
||||
// We *DO NOT* need a write though, because I think that lock outs
|
||||
// and rate limits are *per server* and *in memory* only.
|
||||
let qs_read = self.qs.read();
|
||||
//
|
||||
// Check anything needed? Get the current auth-session-id from request
|
||||
// because it associates to the nonce's etc which were all cached.
|
||||
|
||||
|
@ -144,7 +146,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
]));
|
||||
|
||||
// Get the first / single entry we expect here ....
|
||||
let entry = match qs_read.internal_search(au, filter_entry) {
|
||||
let entry = match self.qs_read.internal_search(au, filter_entry) {
|
||||
Ok(mut entries) => {
|
||||
// Get only one entry out ...
|
||||
if entries.len() >= 2 {
|
||||
|
@ -164,7 +166,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
// typing and functionality so we can assess what auth types can
|
||||
// continue, and helps to keep non-needed entry specific data
|
||||
// out of the LRU.
|
||||
let account = Account::try_from_entry_ro(au, entry, &qs_read)?;
|
||||
let account = Account::try_from_entry_ro(au, entry, &self.qs_read)?;
|
||||
let auth_session = AuthSession::new(account, init.appid.clone());
|
||||
|
||||
// Get the set of mechanisms that can proceed. This is tied
|
||||
|
@ -180,7 +182,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
self.sessions.insert(sessionid, auth_session);
|
||||
|
||||
// Debugging: ensure we really inserted ...
|
||||
assert!(self.sessions.get(&sessionid).is_some());
|
||||
debug_assert!(self.sessions.get(&sessionid).is_some());
|
||||
|
||||
Ok(AuthResult {
|
||||
sessionid,
|
||||
|
@ -210,6 +212,27 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn auth_unix(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
uae: &UnixUserAuthEvent,
|
||||
_ct: Duration,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// TODO #59: Implement soft lock checking for unix creds here!
|
||||
|
||||
// Get the entry/target we are working on.
|
||||
let account_entry = try_audit!(au, self.qs_read.internal_search_uuid(au, &uae.target));
|
||||
|
||||
// Get their account
|
||||
let account = try_audit!(
|
||||
au,
|
||||
UnixUserAccount::try_from_entry_ro(au, account_entry, &self.qs_read)
|
||||
);
|
||||
|
||||
// Validate the unix_pw - this checks the account/cred lock states.
|
||||
account.verify_unix_credential(au, uae.cleartext.as_str())
|
||||
}
|
||||
|
||||
pub fn commit(self) -> Result<(), OperationError> {
|
||||
self.sessions.commit();
|
||||
Ok(())
|
||||
|
@ -271,51 +294,26 @@ impl IdmServerProxyReadTransaction {
|
|||
}
|
||||
|
||||
impl<'a> IdmServerProxyWriteTransaction<'a> {
|
||||
pub fn set_account_password(
|
||||
&mut self,
|
||||
fn check_password_quality(
|
||||
&self,
|
||||
au: &mut AuditScope,
|
||||
pce: &PasswordChangeEvent,
|
||||
cleartext: &str,
|
||||
related_inputs: &[&str],
|
||||
) -> Result<(), OperationError> {
|
||||
// Get the account
|
||||
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target));
|
||||
let account = try_audit!(
|
||||
au,
|
||||
Account::try_from_entry_rw(au, account_entry, &self.qs_write)
|
||||
);
|
||||
// Ask if tis all good - this step checks pwpolicy and such
|
||||
|
||||
// Deny the change if the account is anonymous!
|
||||
if account.is_anonymous() {
|
||||
return Err(OperationError::SystemProtectedObject);
|
||||
}
|
||||
|
||||
// Question: Is it a security issue to reveal pw policy checks BEFORE permission is
|
||||
// determined over the credential modification?
|
||||
//
|
||||
// I don't think so - because we should only be showing how STRONG the pw is ...
|
||||
|
||||
// password strength and badlisting is always global, rather than per-pw-policy.
|
||||
// pw-policy as check on the account is about requirements for mfa for example.
|
||||
//
|
||||
|
||||
// is the password at least 10 char?
|
||||
if pce.cleartext.len() < PW_MIN_LENGTH {
|
||||
if cleartext.len() < PW_MIN_LENGTH {
|
||||
return Err(OperationError::PasswordTooShort(PW_MIN_LENGTH));
|
||||
}
|
||||
|
||||
// does the password pass zxcvbn?
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
let related: Vec<&str> = vec![
|
||||
account.name.as_str(),
|
||||
account.displayname.as_str(),
|
||||
account.spn.as_str(),
|
||||
];
|
||||
|
||||
let entropy = try_audit!(
|
||||
au,
|
||||
zxcvbn::zxcvbn(pce.cleartext.as_str(), related.as_slice())
|
||||
.map_err(|_| OperationError::PasswordEmpty)
|
||||
zxcvbn::zxcvbn(cleartext, related_inputs).map_err(|_| OperationError::PasswordEmpty)
|
||||
);
|
||||
|
||||
// check account pwpolicy (for 3 or 4)? Do we need pw strength beyond this
|
||||
|
@ -341,7 +339,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
|
||||
// check a password badlist to eliminate more content
|
||||
// we check the password as "lower case" to help eliminate possibilities
|
||||
let lc_password = PartialValue::new_iutf8s(pce.cleartext.as_str());
|
||||
let lc_password = PartialValue::new_iutf8s(cleartext);
|
||||
let badlist_entry = try_audit!(
|
||||
au,
|
||||
self.qs_write.internal_search_uuid(au, &UUID_SYSTEM_CONFIG)
|
||||
|
@ -351,6 +349,44 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
return Err(OperationError::PasswordBadListed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_account_password(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
pce: &PasswordChangeEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
// Get the account
|
||||
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target));
|
||||
let account = try_audit!(
|
||||
au,
|
||||
Account::try_from_entry_rw(au, account_entry, &self.qs_write)
|
||||
);
|
||||
// Ask if tis all good - this step checks pwpolicy and such
|
||||
|
||||
// Deny the change if the account is anonymous!
|
||||
if account.is_anonymous() {
|
||||
return Err(OperationError::SystemProtectedObject);
|
||||
}
|
||||
|
||||
// Question: Is it a security issue to reveal pw policy checks BEFORE permission is
|
||||
// determined over the credential modification?
|
||||
//
|
||||
// I don't think so - because we should only be showing how STRONG the pw is ...
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
let related_inputs: Vec<&str> = vec![
|
||||
account.name.as_str(),
|
||||
account.displayname.as_str(),
|
||||
account.spn.as_str(),
|
||||
];
|
||||
|
||||
try_audit!(
|
||||
au,
|
||||
self.check_password_quality(au, pce.cleartext.as_str(), related_inputs.as_slice())
|
||||
);
|
||||
|
||||
// it returns a modify
|
||||
let modlist = try_audit!(
|
||||
au,
|
||||
|
@ -375,6 +411,58 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_unix_account_password(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
pce: &UnixPasswordChangeEvent,
|
||||
) -> Result<(), OperationError> {
|
||||
// Get the account
|
||||
let account_entry = try_audit!(au, self.qs_write.internal_search_uuid(au, &pce.target));
|
||||
// Assert the account is unix and valid.
|
||||
let account = try_audit!(
|
||||
au,
|
||||
UnixUserAccount::try_from_entry_rw(au, account_entry, &self.qs_write)
|
||||
);
|
||||
// Ask if tis all good - this step checks pwpolicy and such
|
||||
|
||||
// Deny the change if the account is anonymous!
|
||||
if account.is_anonymous() {
|
||||
return Err(OperationError::SystemProtectedObject);
|
||||
}
|
||||
|
||||
// Get related inputs, such as account name, email, etc.
|
||||
let related_inputs: Vec<&str> = vec![
|
||||
account.name.as_str(),
|
||||
account.displayname.as_str(),
|
||||
account.spn.as_str(),
|
||||
];
|
||||
|
||||
try_audit!(
|
||||
au,
|
||||
self.check_password_quality(au, pce.cleartext.as_str(), related_inputs.as_slice())
|
||||
);
|
||||
|
||||
// it returns a modify
|
||||
let modlist = try_audit!(au, account.gen_password_mod(pce.cleartext.as_str()));
|
||||
audit_log!(au, "processing change {:?}", modlist);
|
||||
// given the new credential generate a modify
|
||||
// We use impersonate here to get the event from ae
|
||||
try_audit!(
|
||||
au,
|
||||
self.qs_write.impersonate_modify(
|
||||
au,
|
||||
// Filter as executed
|
||||
filter!(f_eq("uuid", PartialValue::new_uuidr(&pce.target))),
|
||||
// Filter as intended (acp)
|
||||
filter_all!(f_eq("uuid", PartialValue::new_uuidr(&pce.target))),
|
||||
modlist,
|
||||
&pce.event,
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn recover_account(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
|
@ -493,7 +581,7 @@ mod tests {
|
|||
use crate::event::{AuthEvent, AuthResult, CreateEvent, ModifyEvent};
|
||||
use crate::idm::event::{
|
||||
PasswordChangeEvent, RadiusAuthTokenEvent, RegenerateRadiusSecretEvent,
|
||||
UnixGroupTokenEvent, UnixUserTokenEvent,
|
||||
UnixGroupTokenEvent, UnixPasswordChangeEvent, UnixUserAuthEvent, UnixUserTokenEvent,
|
||||
};
|
||||
use crate::modify::{Modify, ModifyList};
|
||||
use crate::value::{PartialValue, Value};
|
||||
|
@ -934,4 +1022,66 @@ mod tests {
|
|||
assert!(tok_g.spn == "admin@example.com");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_simple_unix_password_reset() {
|
||||
run_idm_test!(|_qs: &QueryServer, idms: &IdmServer, au: &mut AuditScope| {
|
||||
let mut idms_prox_write = idms.proxy_write();
|
||||
// make the admin a valid posix account
|
||||
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());
|
||||
|
||||
let pce = UnixPasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
|
||||
assert!(idms_prox_write.set_unix_account_password(au, &pce).is_ok());
|
||||
assert!(idms_prox_write.set_unix_account_password(au, &pce).is_ok());
|
||||
assert!(idms_prox_write.commit(au).is_ok());
|
||||
|
||||
let mut idms_write = idms.write();
|
||||
// Check auth verification of the password
|
||||
|
||||
let uuae_good = UnixUserAuthEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
let a1 = idms_write.auth_unix(au, &uuae_good, Duration::from_secs(TEST_CURRENT_TIME));
|
||||
match a1 {
|
||||
Ok(Some(_tok)) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
// Check bad password
|
||||
let uuae_bad = UnixUserAuthEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD_INC);
|
||||
let a2 = idms_write.auth_unix(au, &uuae_bad, Duration::from_secs(TEST_CURRENT_TIME));
|
||||
match a2 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
assert!(idms_write.commit().is_ok());
|
||||
|
||||
// Check deleting the password
|
||||
let mut idms_prox_write = idms.proxy_write();
|
||||
let me_purge_up = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq("name", PartialValue::new_iutf8s("admin"))),
|
||||
ModifyList::new_list(vec![Modify::Purged("unix_password".to_string())]),
|
||||
)
|
||||
};
|
||||
assert!(idms_prox_write.qs_write.modify(au, &me_purge_up).is_ok());
|
||||
assert!(idms_prox_write.commit(au).is_ok());
|
||||
|
||||
// And auth should now fail due to the lack of PW material
|
||||
let mut idms_write = idms.write();
|
||||
let a3 = idms_write.auth_unix(au, &uuae_good, Duration::from_secs(TEST_CURRENT_TIME));
|
||||
match a3 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
assert!(idms_write.commit().is_ok());
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::audit::AuditScope;
|
||||
use crate::constants::UUID_ANONYMOUS;
|
||||
use crate::credential::Credential;
|
||||
use crate::entry::{Entry, EntryCommitted, EntryReduced, EntryValid};
|
||||
use crate::server::{QueryServerReadTransaction, QueryServerTransaction};
|
||||
use crate::value::PartialValue;
|
||||
use crate::modify::{ModifyInvalid, ModifyList};
|
||||
use crate::server::{
|
||||
QueryServerReadTransaction, QueryServerTransaction, QueryServerWriteTransaction,
|
||||
};
|
||||
use crate::value::{PartialValue, Value};
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
||||
|
||||
|
@ -19,6 +24,7 @@ pub(crate) struct UnixUserAccount {
|
|||
pub shell: Option<String>,
|
||||
pub sshkeys: Vec<String>,
|
||||
pub groups: Vec<UnixGroup>,
|
||||
cred: Option<Credential>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
@ -28,50 +34,48 @@ lazy_static! {
|
|||
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) {
|
||||
macro_rules! try_from_entry {
|
||||
($value:expr, $groups:expr) => {{
|
||||
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) {
|
||||
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(|| {
|
||||
let name = $value.get_ava_single_string("name").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
|
||||
let spn = value
|
||||
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 uuid = *$value.get_uuid();
|
||||
|
||||
let displayname = value.get_ava_single_string("displayname").ok_or_else(|| {
|
||||
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(|| {
|
||||
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 shell = $value.get_ava_single_string("loginshell");
|
||||
|
||||
let sshkeys = value.get_ava_ssh_pubkeys("ssh_publickey");
|
||||
let sshkeys = $value.get_ava_ssh_pubkeys("ssh_publickey");
|
||||
|
||||
let groups = UnixGroup::try_from_account_entry_red_ro(au, &value, qs)?;
|
||||
let cred = $value
|
||||
.get_ava_single_credential("unix_password")
|
||||
.map(|v| v.clone());
|
||||
|
||||
Ok(UnixUserAccount {
|
||||
name,
|
||||
|
@ -81,8 +85,38 @@ impl UnixUserAccount {
|
|||
gidnumber,
|
||||
shell,
|
||||
sshkeys,
|
||||
groups,
|
||||
groups: $groups,
|
||||
cred,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixUserAccount {
|
||||
pub(crate) fn try_from_entry_rw(
|
||||
au: &mut AuditScope,
|
||||
value: Entry<EntryValid, EntryCommitted>,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = UnixGroup::try_from_account_entry_rw(au, &value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_entry_ro(
|
||||
au: &mut AuditScope,
|
||||
value: Entry<EntryValid, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = UnixGroup::try_from_account_entry_ro(au, &value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_entry_reduced(
|
||||
au: &mut AuditScope,
|
||||
value: Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Self, OperationError> {
|
||||
let groups = UnixGroup::try_from_account_entry_red_ro(au, &value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
pub(crate) fn to_unixusertoken(&self) -> Result<UnixUserToken, OperationError> {
|
||||
|
@ -100,6 +134,46 @@ impl UnixUserAccount {
|
|||
sshkeys: self.sshkeys.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_anonymous(&self) -> bool {
|
||||
self.uuid == *UUID_ANONYMOUS
|
||||
}
|
||||
|
||||
pub(crate) fn gen_password_mod(
|
||||
&self,
|
||||
cleartext: &str,
|
||||
) -> Result<ModifyList<ModifyInvalid>, OperationError> {
|
||||
let ncred = Credential::new_password_only(cleartext);
|
||||
let vcred = Value::new_credential("unix", ncred);
|
||||
Ok(ModifyList::new_purge_and_set("unix_password", vcred))
|
||||
}
|
||||
|
||||
pub(crate) fn verify_unix_credential(
|
||||
&self,
|
||||
_au: &mut AuditScope,
|
||||
cleartext: &str,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// TODO #59: Is the cred locked?
|
||||
// is the cred some or none?
|
||||
match &self.cred {
|
||||
Some(cred) => match &cred.password {
|
||||
Some(pw) => {
|
||||
if pw.verify(cleartext) {
|
||||
Some(self.to_unixusertoken()).transpose()
|
||||
} else {
|
||||
// Failed to auth
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
// We have a cred but it's not a password, that's weird
|
||||
None => Err(OperationError::InvalidAccountState(
|
||||
"non-password cred type?".to_string(),
|
||||
)),
|
||||
},
|
||||
// They don't have a unix cred, fail the auth.
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -150,41 +224,37 @@ macro_rules! try_from_group_e {
|
|||
}};
|
||||
}
|
||||
|
||||
impl UnixGroup {
|
||||
pub fn try_from_account_entry_red_ro(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
macro_rules! try_from_account_group_e {
|
||||
($au:expr, $value:expr, $qs:expr) => {{
|
||||
// First synthesise the self-group from the account.
|
||||
// We have already checked these, but paranoia is better than
|
||||
// complacency.
|
||||
if !value.attribute_value_pres("class", &PVCLASS_ACCOUNT) {
|
||||
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) {
|
||||
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(|| {
|
||||
let name = $value.get_ava_single_string("name").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: name".to_string())
|
||||
})?;
|
||||
|
||||
let spn = value
|
||||
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 uuid = *$value.get_uuid();
|
||||
|
||||
let gidnumber = value.get_ava_single_uint32("gidnumber").ok_or_else(|| {
|
||||
let gidnumber = $value.get_ava_single_uint32("gidnumber").ok_or_else(|| {
|
||||
OperationError::InvalidAccountState("Missing attribute: gidnumber".to_string())
|
||||
})?;
|
||||
|
||||
|
@ -196,7 +266,7 @@ impl UnixGroup {
|
|||
uuid,
|
||||
};
|
||||
|
||||
match value.get_ava_reference_uuid("memberof") {
|
||||
match $value.get_ava_reference_uuid("memberof") {
|
||||
Some(l) => {
|
||||
let f = filter!(f_and!([
|
||||
f_eq("class", PartialValue::new_class("posixgroup")),
|
||||
|
@ -207,7 +277,7 @@ impl UnixGroup {
|
|||
.collect()
|
||||
)
|
||||
]));
|
||||
let ges: Vec<_> = try_audit!(au, qs.internal_search(au, f));
|
||||
let ges: Vec<_> = $qs.internal_search($au, f)?;
|
||||
let groups: Result<Vec<_>, _> = iter::once(Ok(upg))
|
||||
.chain(ges.into_iter().map(UnixGroup::try_from_entry))
|
||||
.collect();
|
||||
|
@ -218,6 +288,32 @@ impl UnixGroup {
|
|||
Ok(vec![upg])
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl UnixGroup {
|
||||
pub fn try_from_account_entry_rw(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryValid, EntryCommitted>,
|
||||
qs: &QueryServerWriteTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
|
||||
pub fn try_from_account_entry_ro(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryValid, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
|
||||
pub fn try_from_account_entry_red_ro(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
qs: &QueryServerReadTransaction,
|
||||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
|
||||
pub fn try_from_entry_reduced(
|
||||
|
|
|
@ -18,9 +18,9 @@ mod utils;
|
|||
mod async_log;
|
||||
#[macro_use]
|
||||
mod audit;
|
||||
mod be;
|
||||
pub mod be;
|
||||
pub mod constants;
|
||||
mod credential;
|
||||
pub mod credential;
|
||||
mod entry;
|
||||
mod event;
|
||||
mod filter;
|
||||
|
|
|
@ -100,7 +100,6 @@ pub trait QueryServerTransaction {
|
|||
Ok(entries_filtered)
|
||||
}
|
||||
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
au: &mut AuditScope,
|
||||
|
@ -1734,6 +1733,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_GIDNUMBER,
|
||||
JSON_SCHEMA_ATTR_BADLIST_PASSWORD,
|
||||
JSON_SCHEMA_ATTR_LOGINSHELL,
|
||||
JSON_SCHEMA_ATTR_UNIX_PASSWORD,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||
|
|
Loading…
Reference in a new issue