20200218 pam (#189)

Add support for unix_password handling, and pam authentication for services.
This commit is contained in:
Firstyear 2020-02-29 14:02:14 +10:00 committed by GitHub
parent b048115698
commit 5a9ad39d6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 3024 additions and 312 deletions

328
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,10 @@ members = [
"kanidm_tools",
"kanidm_unix_int",
"kanidm_unix_int/nss_kanidm",
"kanidm_unix_int/pam_kanidm"
"kanidm_unix_int/pam_kanidm",
]
exclude = [
"kanidm_unix_int/pam_tester"
]

View file

@ -9,3 +9,5 @@ vendor-prep:
cargo vendor
tar -czf vendor.tar.gz vendor
doc-local:
cargo doc --document-private-items

View file

@ -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

View file

@ -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
}
}

View file

@ -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())
}

View file

@ -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.

View file

@ -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) => {

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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
}
}

View 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,
}

View 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
}
}

View 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
}
}

View 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)
}
}
};
}

View 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;

View 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
View 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"

View 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]

View 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 =/");
}
}

View file

@ -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 {

View file

@ -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"),
_ => {

View file

@ -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"),
_ => {

View file

@ -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) };

View file

@ -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!"),
_ => {

View file

@ -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(&gt1, 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(&gt2, 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(&gt1, 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());
}
}

View file

@ -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;

View file

@ -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);

View 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);
}
}
}

View 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),
})
}
}

View file

@ -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,
}

View file

@ -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);
})
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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"

View file

@ -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| {

View file

@ -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(),

View file

@ -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:

View file

@ -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,
})
}
}

View file

@ -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());
})
}
}

View file

@ -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(

View file

@ -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;

View file

@ -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,