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