diff --git a/Cargo.lock b/Cargo.lock index e7688441b..801abd69f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1443,9 +1443,9 @@ dependencies = [ [[package]] name = "http-client" -version = "6.3.4" +version = "6.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98c12a6a451357392f3307325e9a15cbd27451abdaad96e74c30ea8786f615c4" +checksum = "5566ecc26bc6b04e773e680d66141fced78e091ad818e420d726c152b05a64ff" dependencies = [ "async-trait", "cfg-if 1.0.0", @@ -2232,7 +2232,6 @@ dependencies = [ name = "pam_kanidm" version = "1.1.0-alpha.3" dependencies = [ - "async-std", "kanidm_unix_int", "libc", ] @@ -2634,14 +2633,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] @@ -2655,9 +2653,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "remove_dir_all" @@ -3257,15 +3255,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "tide" version = "0.15.1" @@ -3487,9 +3476,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-bidi" diff --git a/Cargo.toml b/Cargo.toml index 1d19d8c00..ec4bcf020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,15 @@ exclude = [ "kanidm_unix_int/pam_tester" ] + +[patch.crates-io] +# tokio = { path = "../../tokio/tokio" } +# tokio-util = { path = "../../tokio/tokio-util" } +# tokio = { git = "https://github.com/Firstyear/tokio.git", rev = "aa6fb48d9a1f3652ee79e3b018a2b9d0c9f89c1e" } +# tokio-util = { git = "https://github.com/Firstyear/tokio.git", rev = "aa6fb48d9a1f3652ee79e3b018a2b9d0c9f89c1e" } + +# concread = { path = "../concread" } +# idlset = { path = "../idlset" } +# ldap3_server = { path = "../ldap3_server" } +# webauthn-rs = { path = "../webauthn-rs" } +# webauthn-authenticator-rs = { path = "../webauthn-authenticator-rs" } diff --git a/designs/unixd_homes_task.rst b/designs/unixd_homes_task.rst new file mode 100644 index 000000000..6bf00050f --- /dev/null +++ b/designs/unixd_homes_task.rst @@ -0,0 +1,132 @@ +unixd homes task +---------------- + +Kanidm attempts to promote uuid's as the primary foreign key that should be +used by applications. A classic feature of pam and nsswitch tools is to create +the home directory of the account on first login. + +Because of these two things, we previously would have to ask deployments to choose between +the following attributes for home directory names. + +* uuid - the preferred foreign key, but not user-friendly. +* name/spn - user friendly, but will break on account rename. + +The unixd tasks daemon is inspired by oddjobd, and allows us to create home directories +with awareness of this problem. + +home directory design +--------------------- + +On login, the tasks daemon uses the value of "home_attr" as the name of the +home directory/folder. If present, the value of "home_alias" is used in getent +responses, and a symlink from "home_alias" is made to home attr. For example: + +:: + + home_attr = uuid + + getent passwd + home = /home/6a159739-93f0-4bff-bdfb-6044c1bab55c + + /home/6a159739-93f0-4bff-bdfb-6044c1bab55c + + +:: + + home_attr = uuid + home_alias = spn + + getent passwd + home = /home/@ + + /home/6a159739-93f0-4bff-bdfb-6044c1bab55c + /home/@ -> /home/6a159739-93f0-4bff-bdfb-6044c1bab55c + +This allows us to flip the symlink on logins if id/domain is ever changed, without +losing or breaking the content of the home directory. + +tasks daemon design +------------------- + +The current unixd daemon runs as an isolated and unprivileged user. This is because pam/nss +contact the unixd daemon which performs network access on their behalf. Due to this +design, this limits damage in a compromise as the unixd daemon user is not root, and has limited +channels to other processes. + +However, to create home directories, root permissions are required. + +An extra daemon is created that carries this out. This is the unixd-tasks daemon. As it runs +as root, the tasks daemon is an attractive target for "bad people" ™ so careful design around +the security of this is required. + +:: + + ┌───────────────┐ + │ Pam │ /var/run/kanidm-unixd/sock + │ │───────────┐(mode 777) + └───────────────┘ │ + │ ┌──────────────────────────────┐ + │ │ Unixd │ + ├──────────▶│ (isolated user) │ + ┌──────────────┐ │ └──────────────────────────────┘ + │ Nsswitch │ │ ▲ + │ │───────────┘ │ + └──────────────┘ │ + │ + /var/run/kanidm-unixd/tasks-sock │ + (mode 600, isolated user) │ + │ + │ + ┌─────────────────────┐ + │ Unixd Tasks │ + │ (root) │ + └─────────────────────┘ + + +The tasks daemon runs as root and has no network facing elements. It connects to the +unixd daemon via a protected unix socket. The unixd daemon establishes a listening +unix domain socket that only root or itself can access at /var/run/kanidm-unixd/tasks-sock which +the unixd tasks daemon connects to. This is because with systemd dynamic users +the tasks daemon may not know what user account it has to chown sockets to, so it is +not viable for the tasks daemon to create the listening socket with the correct permissions. +This is especially true if the unixd daemon restarts and acquires a new uid, while the tasks +daemon persists. + +The tasks daemon only receives a single datagram, which informs it of the details of +the path and symlinks to create. The daemon filters for a number of path injection attacks +that may be present in the names of the accounts. The Kanidm server also filters for path injections in +usernames. + +The unixd daemon maintains a work queue that it ships to the tasks daemon. This queue is +bounded and if the queue is not being serviced, it proceeds with the login/process +as we must assume the user has *not* configured the tasks daemon on the system. This queue +also prevents memory growth/ddos if we are overloaded by login requests. + +In packaging the tasks daemon will use systemds isolation features to further harden this. For +example: + +:: + + CapabilityBoundingSet=CAP_CHOWN,CAP_FOWNER + SystemCallFilter=@aio @basic-io @chown @file-system @io-event @network-io @sync + ProtectSystem=strict + ReadWritePaths=/home /var/run/kanidm-unixd + RestrictAddressFamilies=AF_UNIX + NoNewPrivileges=true + PrivateTmp=true + PrivateDevices=true + PrivateNetwork=true + ProtectHostname=true + ProtectClock=true + ProtectKernelTunables=true + ProtectKernelModules=true + ProtectKernelLogs=true + ProtectControlGroups=true + MemoryDenyWriteExecute=true + + + +// todo, should be added to unixd +# ProtectHome=¶ + + diff --git a/kanidm_book/src/pam_and_nsswitch.md b/kanidm_book/src/pam_and_nsswitch.md index 381ffa8d8..c5a6ec5f8 100644 --- a/kanidm_book/src/pam_and_nsswitch.md +++ b/kanidm_book/src/pam_and_nsswitch.md @@ -13,6 +13,9 @@ cache is also able to cache missing-entry responses to reduce network traffic and main server load. Additionally, the daemon means that the pam and nsswitch integration libraries can be small, helping to reduce the attack surface of the machine. +Similar, a tasks daemon is also provided that can create home directories on first +login, and supports a number of features related to aliases and links to these +home directories. We recommend you install the client daemon from your system package manager. @@ -25,13 +28,22 @@ You can check the daemon is running on your Linux system with systemctl status kanidm_unixd -This daemon uses connection configuration from /etc/kanidm/config. This is the covered in -client_tools. You can also configure some details of the unixd daemon in /etc/kanidm/unixd. +You can check the privileged tasks daemon is running with + + systemctl status kanidm_unixd_tasks + +> **NOTE** The `kanidm_unixd_tasks` daemon is not required for pam and nsswitch functionality. +> If disabled, your system will function as usual. It is however recommended due to the features +> it provides supporting kanidm's capabilities. + +Both unixd daemons use the connection configuration from /etc/kanidm/config. This is the covered in +client_tools. You can also configure some details of the unixd daemons in /etc/kanidm/unixd. pam_allowed_login_groups = ["posix_group"] default_shell = "/bin/bash" home_prefix = "/home/" home_attr = "uuid" + home_alias = "spn" uid_attr_map = "spn" gid_attr_map = "spn" @@ -47,12 +59,17 @@ a trailing `/`. Defaults to `/home/`. `home_attr` is the default token attribute used for the home directory path. Valid choices are `uuid`, `name`, `spn`. Defaults to `uuid`. +`home_alias` is the default token attribute used for generating symlinks pointing to the users +home directory. If set, this will become the value of the home path +to nss calls. It is recommended you choose a "human friendly" attribute here. +Valid choices are `none`, `uuid`, `name`, `spn`. Defaults to `spn`. + > **NOTICE:** > All users in kanidm can change their name (and their spn) at any time. If you change > `home_attr` from `uuid` you *must* have a plan on how to manage these directory renames > in your system. We recommend that you have a stable id (like the uuid) and symlinks -> from the name to the uuid folder. The project plans to add automatic support for this -> with https://github.com/kanidm/kanidm/issues/180 +> from the name to the uuid folder. Automatic support is provided for this via the unixd +> tasks daemon, as documented here. `uid_attr_map` chooses which attribute is used for domain local users in presentation. Defaults to `spn`. Users from a trust will always use spn. @@ -140,17 +157,19 @@ Each of these controls one of the four stages of pam. The content should look li 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 + 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 +> **WARNING:** Ensure that `pam_mkhomedir` or `pam_oddjobd` are *not* present in your pam configuration +> these interfer with the correct operation of the kanidm tasks daemon. + ### Fedora TBD @@ -170,6 +189,8 @@ And add the lines: Then restart the kanidm-unixd.service. +The same pattern is true for the kanidm-unixd-tasks.service daemon. + To debug the pam module interactions add `debug` to the module arguments such as: auth sufficient pam_kanidm.so debug @@ -179,6 +200,9 @@ To debug the pam module interactions add `debug` to the module arguments such as Check that the /var/run/kanidm-unixd/sock is 777, and that non-root readers can see it with ls or other tools. +Ensure that /var/run/kanidm-unixd/task_sock is 700, and that it is owned by the kanidm unixd +process user. + ### Check you can access the kanidm server You can check this with the client tools: diff --git a/kanidm_client/Cargo.toml b/kanidm_client/Cargo.toml index fe0dddcf0..89cd4d871 100644 --- a/kanidm_client/Cargo.toml +++ b/kanidm_client/Cargo.toml @@ -20,19 +20,12 @@ serde_derive = "1.0" toml = "0.5" uuid = { version = "0.8", features = ["serde", "v4"] } url = "2.1.1" -# users = "0.10" -# webauthn-authenticator-rs = "^0.1.2" -# webauthn-authenticator-rs = { path = "../../webauthn-authenticator-rs" } - webauthn-rs = "0.3.0-alpha.1" -# webauthn-rs = { path = "../../webauthn-rs" } [dev-dependencies] -# tokio = { version = "0.2", features = ["full"] } tokio = { version = "1", features = ["rt", "net", "time", "macros", "sync", "signal"] } kanidm = { path = "../kanidmd" } futures = "0.3" async-std = "1.6" webauthn-authenticator-rs = "0.3.0-alpha.1" -# webauthn-authenticator-rs = { path = "../../webauthn-authenticator-rs" } diff --git a/kanidm_proto/Cargo.toml b/kanidm_proto/Cargo.toml index 95610664b..cf0c23813 100644 --- a/kanidm_proto/Cargo.toml +++ b/kanidm_proto/Cargo.toml @@ -17,7 +17,6 @@ zxcvbn = { version = "2.0", features = ["ser"] } base32 = "0.4" thiserror = "1.0" webauthn-rs = "0.3.0-alpha.5" -# webauthn-rs = { path = "../../webauthn-rs" } [dev-dependencies] serde_json = "1.0" diff --git a/kanidm_tools/Cargo.toml b/kanidm_tools/Cargo.toml index ab4b94c68..4c5428cbb 100644 --- a/kanidm_tools/Cargo.toml +++ b/kanidm_tools/Cargo.toml @@ -44,7 +44,6 @@ qrcode = { version = "0.12", default-features = false } zxcvbn = "2.0" webauthn-authenticator-rs = "^0.3.0-alpha.6" -# webauthn-authenticator-rs = { path = "../../webauthn-authenticator-rs" } [build-dependencies] structopt = { version = "0.3", default-features = false } diff --git a/kanidm_unix_int/Cargo.toml b/kanidm_unix_int/Cargo.toml index a0c4fa2a7..2e50f69da 100644 --- a/kanidm_unix_int/Cargo.toml +++ b/kanidm_unix_int/Cargo.toml @@ -19,6 +19,10 @@ path = "src/lib.rs" name = "kanidm_unixd" path = "src/daemon.rs" +[[bin]] +name = "kanidm_unixd_tasks" +path = "src/tasks_daemon.rs" + [[bin]] name = "kanidm_ssh_authorizedkeys" path = "src/ssh_authorizedkeys.rs" @@ -45,8 +49,10 @@ kanidm_proto = { path = "../kanidm_proto", version = "1.1.0-alpha" } kanidm = { path = "../kanidmd", version = "1.1.0-alpha" } toml = "0.5" rpassword = "5.0" -tokio = { version = "1", features = ["rt", "macros", "sync", "time", "net", "io-util", "signal"] } +## Removed "signal" +tokio = { version = "1", features = ["rt", "macros", "sync", "time", "net", "io-util"] } tokio-util = { version = "0.6", features = ["codec"] } + futures = "0.3" bytes = "1.0" diff --git a/kanidm_unix_int/nss_kanidm/src/lib.rs b/kanidm_unix_int/nss_kanidm/src/lib.rs index 8c4e60031..ddc1e08dc 100644 --- a/kanidm_unix_int/nss_kanidm/src/lib.rs +++ b/kanidm_unix_int/nss_kanidm/src/lib.rs @@ -13,7 +13,7 @@ extern crate libnss; #[macro_use] extern crate lazy_static; -use kanidm_unix_common::client::call_daemon_blocking; +use kanidm_unix_common::client_sync::call_daemon_blocking; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, NssGroup, NssUser}; diff --git a/kanidm_unix_int/pam_kanidm/Cargo.toml b/kanidm_unix_int/pam_kanidm/Cargo.toml index 4df465d62..4fc96a3d0 100644 --- a/kanidm_unix_int/pam_kanidm/Cargo.toml +++ b/kanidm_unix_int/pam_kanidm/Cargo.toml @@ -11,5 +11,4 @@ path = "src/lib.rs" [dependencies] kanidm_unix_int = { path = "../", version = "1.1.0-alpha" } -async-std = "1.6" libc = "0.2" diff --git a/kanidm_unix_int/pam_kanidm/src/lib.rs b/kanidm_unix_int/pam_kanidm/src/lib.rs index 4b3d3b197..9a4541477 100644 --- a/kanidm_unix_int/pam_kanidm/src/lib.rs +++ b/kanidm_unix_int/pam_kanidm/src/lib.rs @@ -20,8 +20,7 @@ use std::collections::BTreeSet; use std::convert::TryFrom; use std::ffi::CStr; // use std::os::raw::c_char; -use async_std::task; -use kanidm_unix_common::client::call_daemon; +use kanidm_unix_common::client_sync::call_daemon_blocking; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse}; @@ -92,7 +91,7 @@ impl PamHooks for PamKanidm { let req = ClientRequest::PamAccountAllowed(account_id); // PamResultCode::PAM_IGNORE - match task::block_on(call_daemon(cfg.sock_path.as_str(), req)) { + match call_daemon_blocking(cfg.sock_path.as_str(), req) { Ok(r) => match r { ClientResponse::PamStatus(Some(true)) => { // println!("PAM_SUCCESS"); @@ -201,7 +200,7 @@ impl PamHooks for PamKanidm { }; let req = ClientRequest::PamAuthenticate(account_id, authtok); - match task::block_on(call_daemon(cfg.sock_path.as_str(), req)) { + match call_daemon_blocking(cfg.sock_path.as_str(), req) { Ok(r) => match r { ClientResponse::PamStatus(Some(true)) => { // println!("PAM_SUCCESS"); @@ -264,7 +263,7 @@ impl PamHooks for PamKanidm { PamResultCode::PAM_SUCCESS } - fn sm_open_session(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { + 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, @@ -275,7 +274,35 @@ impl PamHooks for PamKanidm { println!("args -> {:?}", args); println!("opts -> {:?}", opts); } - PamResultCode::PAM_SUCCESS + + 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::PamAccountBeginSession(account_id); + + match call_daemon_blocking(cfg.sock_path.as_str(), req) { + Ok(ClientResponse::Ok) => { + // println!("PAM_SUCCESS"); + PamResultCode::PAM_SUCCESS + } + other => { + if opts.debug { + println!("PAM_IGNORE -> {:?}", other); + } + PamResultCode::PAM_IGNORE + } + } } fn sm_setcred(_pamh: &PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { diff --git a/kanidm_unix_int/src/cache.rs b/kanidm_unix_int/src/cache.rs index c462ddae5..6f89ebd7a 100644 --- a/kanidm_unix_int/src/cache.rs +++ b/kanidm_unix_int/src/cache.rs @@ -1,6 +1,6 @@ use crate::db::Db; use crate::unix_config::{HomeAttr, UidAttr}; -use crate::unix_proto::{NssGroup, NssUser}; +use crate::unix_proto::{HomeDirectoryInfo, NssGroup, NssUser}; use kanidm_client::asynchronous::KanidmAsyncClient; use kanidm_client::ClientError; use kanidm_proto::v1::{OperationError, UnixGroupToken, UnixUserToken}; @@ -37,6 +37,7 @@ pub struct CacheLayer { default_shell: String, home_prefix: String, home_attr: HomeAttr, + home_alias: Option, uid_attr_map: UidAttr, gid_attr_map: UidAttr, nxcache: Mutex>, @@ -65,6 +66,7 @@ impl CacheLayer { default_shell: String, home_prefix: String, home_attr: HomeAttr, + home_alias: Option, uid_attr_map: UidAttr, gid_attr_map: UidAttr, ) -> Result { @@ -88,6 +90,7 @@ impl CacheLayer { default_shell, home_prefix, home_attr, + home_alias, uid_attr_map, gid_attr_map, nxcache: Mutex::new(LruCache::new(NXCACHE_SIZE)), @@ -593,17 +596,34 @@ impl CacheLayer { .unwrap_or_else(|| Vec::with_capacity(0))) } + #[inline(always)] + fn token_homedirectory_alias(&self, token: &UnixUserToken) -> Option { + self.home_alias.map(|t| match t { + // If we have an alias. use it. + HomeAttr::Uuid => token.uuid.as_str().to_string(), + HomeAttr::Spn => token.spn.as_str().to_string(), + HomeAttr::Name => token.name.as_str().to_string(), + }) + } + + #[inline(always)] + fn token_homedirectory_attr(&self, token: &UnixUserToken) -> String { + match self.home_attr { + HomeAttr::Uuid => token.uuid.as_str().to_string(), + HomeAttr::Spn => token.spn.as_str().to_string(), + HomeAttr::Name => token.name.as_str().to_string(), + } + } + #[inline(always)] fn token_homedirectory(&self, token: &UnixUserToken) -> String { - format!( - "{}{}", - self.home_prefix, - match self.home_attr { - HomeAttr::Uuid => token.uuid.as_str(), - HomeAttr::Spn => token.spn.as_str(), - HomeAttr::Name => token.name.as_str(), - } - ) + self.token_homedirectory_alias(token) + .unwrap_or_else(|| self.token_homedirectory_attr(token)) + } + + #[inline(always)] + fn token_abs_homedirectory(&self, token: &UnixUserToken) -> String { + format!("{}{}", self.home_prefix, self.token_homedirectory(token)) } #[inline(always)] @@ -619,7 +639,7 @@ impl CacheLayer { self.get_cached_usertokens().await.map(|l| { l.into_iter() .map(|tok| NssUser { - homedir: self.token_homedirectory(&tok), + homedir: self.token_abs_homedirectory(&tok), name: self.token_uidattr(&tok), gid: tok.gidnumber, gecos: tok.displayname, @@ -632,7 +652,7 @@ impl CacheLayer { async fn get_nssaccount(&self, account_id: Id) -> Result, ()> { let token = self.get_usertoken(account_id).await?; Ok(token.map(|tok| NssUser { - homedir: self.token_homedirectory(&tok), + homedir: self.token_abs_homedirectory(&tok), name: self.token_uidattr(&tok), gid: tok.gidnumber, gecos: tok.displayname, @@ -858,6 +878,21 @@ impl CacheLayer { } } + pub async fn pam_account_beginsession( + &self, + account_id: &str, + ) -> Result, ()> { + let token = self.get_usertoken(Id::Name(account_id.to_string())).await?; + Ok(token.as_ref().map(|tok| HomeDirectoryInfo { + gid: tok.gidnumber, + name: self.token_homedirectory_attr(tok), + aliases: self + .token_homedirectory_alias(tok) + .map(|s| vec![s]) + .unwrap_or_else(Vec::new), + })) + } + pub async fn test_connection(&self) -> bool { let state = self.get_cachestate().await; match state { diff --git a/kanidm_unix_int/src/client.rs b/kanidm_unix_int/src/client.rs index 7a75096e3..d7606509b 100644 --- a/kanidm_unix_int/src/client.rs +++ b/kanidm_unix_int/src/client.rs @@ -1,4 +1,3 @@ -use async_std::task; use bytes::{BufMut, BytesMut}; use futures::SinkExt; use futures::StreamExt; @@ -6,6 +5,7 @@ use std::error::Error; use std::io::Error as IoError; use std::io::ErrorKind; use tokio::net::UnixStream; +// use tokio::runtime::Builder; use tokio_util::codec::Framed; use tokio_util::codec::{Decoder, Encoder}; @@ -68,10 +68,3 @@ pub async fn call_daemon(path: &str, req: ClientRequest) -> Result Result> { - task::block_on(call_daemon(path, req)) -} diff --git a/kanidm_unix_int/src/client_sync.rs b/kanidm_unix_int/src/client_sync.rs new file mode 100644 index 000000000..27df79423 --- /dev/null +++ b/kanidm_unix_int/src/client_sync.rs @@ -0,0 +1,110 @@ +use std::error::Error; +use std::io::Error as IoError; +use std::io::ErrorKind; +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; + +use std::time::{Duration, SystemTime}; + +use crate::unix_proto::{ClientRequest, ClientResponse}; + +const TIMEOUT: u64 = 2000; + +pub fn call_daemon_blocking( + path: &str, + req: ClientRequest, +) -> Result> { + let mut stream = UnixStream::connect(path) + .and_then(|socket| { + socket + .set_read_timeout(Some(Duration::from_millis(TIMEOUT))) + .map(|_| socket) + }) + .and_then(|socket| { + socket + .set_write_timeout(Some(Duration::from_millis(TIMEOUT))) + .map(|_| socket) + }) + .map_err(|e| { + error!("stream setup error -> {:?}", e); + e + }) + .map_err(Box::new)?; + + let data = serde_cbor::to_vec(&req).map_err(|e| { + error!("socket encoding error -> {:?}", e); + Box::new(IoError::new(ErrorKind::Other, "CBOR encode error")) + })?; + // .map_err(Box::new)?; + + stream + .write_all(data.as_slice()) + .and_then(|_| stream.flush()) + .map_err(|e| { + error!("stream write error -> {:?}", e); + e + }) + .map_err(Box::new)?; + + // Now wait on the response. + let start = SystemTime::now(); + let timeout = Duration::from_millis(TIMEOUT); + let mut read_started = false; + let mut data = Vec::with_capacity(1024); + let mut counter = 0; + + loop { + let mut buffer = [0; 1024]; + let durr = SystemTime::now().duration_since(start).map_err(Box::new)?; + if durr > timeout { + error!("Socket timeout"); + // timed out, not enough activity. + break; + } + // Would be a lot easier if we had peek ... + // https://github.com/rust-lang/rust/issues/76923 + match stream.read(&mut buffer) { + Ok(0) => { + if read_started { + debug!("read_started true, we have completed"); + // We're done, no more bytes. + break; + } else { + debug!("Waiting ..."); + // Still can wait ... + continue; + } + } + Ok(count) => { + data.extend_from_slice(&buffer); + counter += count; + if count == 1024 { + debug!("Filled 1024 bytes, looping ..."); + // We have filled the buffer, we need to copy and loop again. + read_started = true; + continue; + } else { + debug!("Filled {} bytes, complete", count); + // We have a partial read, so we are complete. + break; + } + } + Err(e) => { + error!("Steam read failure -> {:?}", e); + // Failure! + return Err(Box::new(e)); + } + } + } + + // Extend from slice fills with 0's, so we need to truncate now. + data.truncate(counter); + + // Now attempt to decode. + let cr = serde_cbor::from_slice::(data.as_slice()).map_err(|e| { + error!("socket encoding error -> {:?}", e); + Box::new(IoError::new(ErrorKind::Other, "CBOR decode error")) + })?; + + Ok(cr) +} diff --git a/kanidm_unix_int/src/constants.rs b/kanidm_unix_int/src/constants.rs index c700923b8..725b8553b 100644 --- a/kanidm_unix_int/src/constants.rs +++ b/kanidm_unix_int/src/constants.rs @@ -1,11 +1,13 @@ use crate::unix_config::{HomeAttr, UidAttr}; pub const DEFAULT_SOCK_PATH: &str = "/var/run/kanidm-unixd/sock"; +pub const DEFAULT_TASK_SOCK_PATH: &str = "/var/run/kanidm-unixd/task_sock"; pub const DEFAULT_DB_PATH: &str = "/var/cache/kanidm-unixd/kanidm.cache.db"; pub const DEFAULT_CONN_TIMEOUT: u64 = 2; pub const DEFAULT_CACHE_TIMEOUT: u64 = 15; pub const DEFAULT_SHELL: &str = "/bin/bash"; pub const DEFAULT_HOME_PREFIX: &str = "/home/"; pub const DEFAULT_HOME_ATTR: HomeAttr = HomeAttr::Uuid; +pub const DEFAULT_HOME_ALIAS: Option = Some(HomeAttr::Spn); pub const DEFAULT_UID_ATTR_MAP: UidAttr = UidAttr::Spn; pub const DEFAULT_GID_ATTR_MAP: UidAttr = UidAttr::Spn; diff --git a/kanidm_unix_int/src/daemon.rs b/kanidm_unix_int/src/daemon.rs index 4f3a32037..249a51e13 100644 --- a/kanidm_unix_int/src/daemon.rs +++ b/kanidm_unix_int/src/daemon.rs @@ -14,6 +14,8 @@ extern crate log; use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid}; use std::fs::metadata; +use std::io::Error as IoError; +use std::io::ErrorKind; use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; @@ -24,7 +26,11 @@ use libc::umask; use std::error::Error; use std::io; use std::sync::Arc; +use std::time::Duration; use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; +use tokio::sync::oneshot; +use tokio::time; use tokio_util::codec::Framed; use tokio_util::codec::{Decoder, Encoder}; @@ -32,10 +38,12 @@ use kanidm_client::KanidmClientBuilder; use kanidm_unix_common::cache::CacheLayer; use kanidm_unix_common::unix_config::KanidmUnixdConfig; -use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse}; +use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, TaskRequest, TaskResponse}; //=== the codec +type AsyncTaskRequest = (TaskRequest, oneshot::Sender<()>); + struct ClientCodec; impl Decoder for ClientCodec { @@ -74,15 +82,100 @@ impl ClientCodec { } } +struct TaskCodec; + +impl Decoder for TaskCodec { + type Item = TaskResponse; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match serde_cbor::from_slice::(&src) { + Ok(msg) => { + // Clear the buffer for the next message. + src.clear(); + Ok(Some(msg)) + } + _ => Ok(None), + } + } +} + +impl Encoder for TaskCodec { + type Error = io::Error; + + fn encode(&mut self, msg: TaskRequest, dst: &mut BytesMut) -> Result<(), Self::Error> { + debug!("Attempting to send request -> {:?} ...", msg); + let data = serde_cbor::to_vec(&msg).map_err(|e| { + error!("socket encoding error -> {:?}", e); + io::Error::new(io::ErrorKind::Other, "CBOR encode error") + })?; + dst.put(data.as_slice()); + Ok(()) + } +} + +impl TaskCodec { + fn new() -> Self { + TaskCodec + } +} + fn rm_if_exist(p: &str) { let _ = std::fs::remove_file(p).map_err(|e| { warn!("attempting to remove {:?} -> {:?}", p, e); }); } +async fn handle_task_client( + stream: UnixStream, + task_channel_tx: &Sender, + task_channel_rx: &mut Receiver, +) -> Result<(), Box> { + // setup the codec + let mut reqs = Framed::new(stream, TaskCodec::new()); + + loop { + // TODO wait on the channel OR the task handler, so we know + // when it closes. + let v = match task_channel_rx.recv().await { + Some(v) => v, + None => return Ok(()), + }; + + debug!("Sending Task -> {:?}", v.0); + + // Write the req to the socket. + if let Err(_e) = reqs.send(v.0.clone()).await { + // re-queue the event if not timed out. + // This is indicated by the one shot being dropped. + if !v.1.is_closed() { + let _ = task_channel_tx + .send_timeout(v, Duration::from_millis(100)) + .await; + } + // now return the error. + return Err(Box::new(IoError::new(ErrorKind::Other, "oh no!"))); + } + + match reqs.next().await { + Some(Ok(TaskResponse::Success)) => { + debug!("Task was acknowledged and completed."); + // Send a result back via the one-shot + // Ignore if it fails. + let _ = v.1.send(()); + } + other => { + error!("Error -> {:?}", other); + return Err(Box::new(IoError::new(ErrorKind::Other, "oh no!"))); + } + } + } +} + async fn handle_client( sock: UnixStream, cachelayer: Arc, + task_channel_tx: &Sender, ) -> Result<(), Box> { debug!("Accepted connection"); @@ -183,6 +276,50 @@ async fn handle_client( .map(ClientResponse::PamStatus) .unwrap_or(ClientResponse::Error) } + ClientRequest::PamAccountBeginSession(account_id) => { + debug!("pam account begin session"); + match cachelayer + .pam_account_beginsession(account_id.as_str()) + .await + { + Ok(Some(info)) => { + let (tx, rx) = oneshot::channel(); + + match task_channel_tx + .send_timeout( + (TaskRequest::HomeDirectory(info), tx), + Duration::from_millis(100), + ) + .await + { + Ok(()) => { + // Now wait for the other end OR + // timeout. + match time::timeout_at( + time::Instant::now() + Duration::from_millis(1000), + rx, + ) + .await + { + Ok(Ok(_)) => { + debug!("Task completed, returning to pam ..."); + ClientResponse::Ok + } + _ => { + // Timeout or other error. + ClientResponse::Error + } + } + } + Err(_) => { + // We could not submit the req. Move on! + ClientResponse::Error + } + } + } + _ => ClientResponse::Error, + } + } ClientRequest::InvalidateCache => { debug!("invalidate cache"); cachelayer @@ -308,6 +445,7 @@ async fn main() { }; rm_if_exist(cfg.sock_path.as_str()); + rm_if_exist(cfg.task_sock_path.as_str()); let cb = cb.connect_timeout(cfg.conn_timeout); @@ -382,6 +520,7 @@ async fn main() { cfg.default_shell.clone(), cfg.home_prefix.clone(), cfg.home_attr, + cfg.home_alias, cfg.uid_attr_map, cfg.gid_attr_map, ) @@ -396,7 +535,7 @@ async fn main() { let cachelayer = Arc::new(cl_inner); - // Set the umask while we open the path + // Set the umask while we open the path for most clients. let before = unsafe { umask(0) }; let listener = match UnixListener::bind(cfg.sock_path.as_str()) { Ok(l) => l, @@ -405,18 +544,71 @@ async fn main() { std::process::exit(1); } }; + // Setup the root-only socket. Take away all others. + let _ = unsafe { umask(0o0077) }; + let task_listener = match UnixListener::bind(cfg.task_sock_path.as_str()) { + Ok(l) => l, + Err(_e) => { + error!("Failed to bind unix socket."); + std::process::exit(1); + } + }; + // Undo it. let _ = unsafe { umask(before) }; + let (task_channel_tx, mut task_channel_rx) = channel(16); + let task_channel_tx = Arc::new(task_channel_tx); + + let task_channel_tx_cln = task_channel_tx.clone(); + + tokio::spawn(async move { + loop { + match task_listener.accept().await { + Ok((socket, _addr)) => { + // Did it come from root? + if let Ok(ucred) = socket.peer_cred() { + if ucred.uid() == 0 { + // all good! + } else { + // move along. + debug!("Task handler not running as root, ignoring ..."); + continue; + } + } else { + // move along. + debug!("Task handler not running as root, ignoring ..."); + continue; + }; + debug!("A task handler has connected."); + // It did? Great, now we can wait and spin on that one + // client. + if let Err(e) = + handle_task_client(socket, &task_channel_tx, &mut task_channel_rx).await + { + error!("Task client error occured; error = {:?}", e); + } + // If they DC we go back to accept. + } + Err(err) => { + error!("Task Accept error -> {:?}", err); + } + } + // done + } + }); + // TODO: Setup a task that handles pre-fetching here. let server = async move { loop { + let tc_tx = task_channel_tx_cln.clone(); match listener.accept().await { Ok((socket, _addr)) => { let cachelayer_ref = cachelayer.clone(); tokio::spawn(async move { - if let Err(e) = handle_client(socket, cachelayer_ref.clone()).await { + if let Err(e) = handle_client(socket, cachelayer_ref.clone(), &tc_tx).await + { error!("an error occured; error = {:?}", e); } }); diff --git a/kanidm_unix_int/src/daemon_status.rs b/kanidm_unix_int/src/daemon_status.rs index 77d3876dc..4c393edc4 100644 --- a/kanidm_unix_int/src/daemon_status.rs +++ b/kanidm_unix_int/src/daemon_status.rs @@ -14,16 +14,15 @@ extern crate log; use log::debug; 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_sync::call_daemon_blocking; use kanidm_unix_common::unix_config::KanidmUnixdConfig; use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse}; include!("./opt/unixd_status.rs"); -#[tokio::main] -async fn main() { +fn main() { let opt = UnixdStatusOpt::from_args(); if opt.debug { ::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug"); @@ -32,7 +31,7 @@ async fn main() { } env_logger::init(); - debug!("Starting cache invalidate tool ..."); + debug!("Starting cache status tool ..."); let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config("/etc/kanidm/unixd") { @@ -45,7 +44,7 @@ async fn main() { let req = ClientRequest::Status; - match block_on(call_daemon(cfg.sock_path.as_str(), req)) { + match call_daemon_blocking(cfg.sock_path.as_str(), req) { Ok(r) => match r { ClientResponse::Ok => info!("working!"), _ => { diff --git a/kanidm_unix_int/src/lib.rs b/kanidm_unix_int/src/lib.rs index 42598149a..5e934b452 100644 --- a/kanidm_unix_int/src/lib.rs +++ b/kanidm_unix_int/src/lib.rs @@ -15,6 +15,7 @@ extern crate log; pub mod cache; pub mod client; +pub mod client_sync; pub mod constants; pub(crate) mod db; pub mod unix_config; diff --git a/kanidm_unix_int/src/tasks_daemon.rs b/kanidm_unix_int/src/tasks_daemon.rs new file mode 100644 index 000000000..cba9acab7 --- /dev/null +++ b/kanidm_unix_int/src/tasks_daemon.rs @@ -0,0 +1,254 @@ +#![deny(warnings)] +#![warn(unused_extern_crates)] +#![deny(clippy::unwrap_used)] +#![deny(clippy::expect_used)] +#![deny(clippy::panic)] +#![deny(clippy::unreachable)] +#![deny(clippy::await_holding_lock)] +#![deny(clippy::needless_pass_by_value)] +#![deny(clippy::trivially_copy_pass_by_ref)] + +#[macro_use] +extern crate log; + +use users::{get_effective_gid, get_effective_uid}; + +use std::os::unix::fs::symlink; + +use libc::{lchown, umask}; +use std::ffi::CString; + +use bytes::{BufMut, BytesMut}; +use futures::SinkExt; +use futures::StreamExt; +use std::fs; +use std::io; +use std::path::Path; +use std::time::Duration; +use tokio::net::UnixStream; +use tokio::time; +use tokio_util::codec::Framed; +use tokio_util::codec::{Decoder, Encoder}; + +use kanidm_unix_common::unix_config::KanidmUnixdConfig; +use kanidm_unix_common::unix_proto::{HomeDirectoryInfo, TaskRequest, TaskResponse}; + +struct TaskCodec; + +impl Decoder for TaskCodec { + type Item = TaskRequest; + type Error = io::Error; + + fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { + match serde_cbor::from_slice::(&src) { + Ok(msg) => { + // Clear the buffer for the next message. + src.clear(); + Ok(Some(msg)) + } + _ => Ok(None), + } + } +} + +impl Encoder for TaskCodec { + type Error = io::Error; + + fn encode(&mut self, msg: TaskResponse, dst: &mut BytesMut) -> Result<(), Self::Error> { + debug!("Attempting to send request -> {:?} ...", msg); + let data = serde_cbor::to_vec(&msg).map_err(|e| { + error!("socket encoding error -> {:?}", e); + io::Error::new(io::ErrorKind::Other, "CBOR encode error") + })?; + dst.put(data.as_slice()); + Ok(()) + } +} + +impl TaskCodec { + fn new() -> Self { + TaskCodec + } +} + +fn create_home_directory(info: &HomeDirectoryInfo, home_prefix: &str) -> Result<(), String> { + // Final sanity check to prevent certain classes of attacks. + let name = info + .name + .trim_start_matches('.') + .replace("/", "") + .replace("\\", ""); + + let home_prefix_path = Path::new(home_prefix); + + // Does our home_prefix actually exist? + if !home_prefix_path.exists() || !home_prefix_path.is_dir() { + return Err("Invalid home_prefix from configuration".to_string()); + } + + // Actually process the request here. + let hd_path_raw = format!("{}{}", home_prefix, name); + let hd_path = Path::new(&hd_path_raw); + + // Assert the resulting named home path is consistent and correct. + if let Some(pp) = hd_path.parent() { + if pp != home_prefix_path { + return Err("Invalid home directory name - not within home_prefix".to_string()); + } + } else { + return Err("Invalid/Corrupt home directory path - no prefix found".to_string()); + } + + let hd_path_os = + CString::new(hd_path_raw.clone()).map_err(|_| "Unable to create c-string".to_string())?; + + // Does the home directory exist? + if !hd_path.exists() { + // Set a umask + let before = unsafe { umask(0o0027) }; + // TODO: Should we copy content from /etc/skel? + // Create the dir + if let Err(e) = fs::create_dir_all(hd_path) { + let _ = unsafe { umask(before) }; + return Err(format!("{:?}", e)); + } + let _ = unsafe { umask(before) }; + + // Change the owner to the gid - remember, kanidm ONLY has gid's, the uid is implied. + + if unsafe { lchown(hd_path_os.as_ptr(), info.gid, info.gid) } != 0 { + return Err("Unable to set ownership".to_string()); + } + } + + let name_rel_path = Path::new(&name); + // Does the aliases exist + for alias in info.aliases.iter() { + // Sanity check the alias. + let alias = alias.replace(".", "").replace("/", "").replace("\\", ""); + let alias_path_raw = format!("{}{}", home_prefix, alias); + let alias_path = Path::new(&alias_path_raw); + + // Assert the resulting alias path is consistent and correct. + if let Some(pp) = alias_path.parent() { + if pp != home_prefix_path { + return Err("Invalid home directory alias - not within home_prefix".to_string()); + } + } else { + return Err("Invalid/Corrupt alias directory path - no prefix found".to_string()); + } + + if alias_path.exists() { + let attr = match fs::symlink_metadata(alias_path) { + Ok(a) => a, + Err(e) => { + return Err(format!("{:?}", e)); + } + }; + + if attr.file_type().is_symlink() { + // Probably need to update it. + if let Err(e) = fs::remove_file(alias_path) { + return Err(format!("{:?}", e)); + } + if let Err(e) = symlink(name_rel_path, alias_path) { + return Err(format!("{:?}", e)); + } + } + } else { + // Does not exist. Create. + if let Err(e) = symlink(name_rel_path, alias_path) { + return Err(format!("{:?}", e)); + } + } + } + Ok(()) +} + +async fn handle_tasks(stream: UnixStream, home_prefix: &str) { + let mut reqs = Framed::new(stream, TaskCodec::new()); + + loop { + match reqs.next().await { + Some(Ok(TaskRequest::HomeDirectory(info))) => { + debug!("Received task -> HomeDirectory({:?})", info); + + let resp = match create_home_directory(&info, home_prefix) { + Ok(()) => TaskResponse::Success, + Err(msg) => TaskResponse::Error(msg), + }; + + // Now send a result. + if let Err(e) = reqs.send(resp).await { + error!("Error -> {:?}", e); + return; + } + // All good, loop. + } + other => { + error!("Error -> {:?}", other); + return; + } + } + } +} + +#[tokio::main] +async fn main() { + // let cuid = get_current_uid(); + // let cgid = get_current_gid(); + // We only need to check effective id + let ceuid = get_effective_uid(); + let cegid = get_effective_gid(); + + if ceuid != 0 || cegid != 0 { + eprintln!("Refusing to run - this process *MUST* operate as root."); + std::process::exit(1); + } + + env_logger::init(); + + let unixd_path = Path::new("/etc/kanidm/unixd"); + let unixd_path_str = match unixd_path.to_str() { + Some(cps) => cps, + None => { + error!("Unable to turn unixd_path to str"); + std::process::exit(1); + } + }; + + let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config(unixd_path) { + Ok(v) => v, + Err(_) => { + error!("Failed to parse {}", unixd_path_str); + std::process::exit(1); + } + }; + + let task_sock_path = cfg.task_sock_path.clone(); + debug!("Attempting to use {} ...", task_sock_path); + + let server = async move { + loop { + info!("Attempting to connect to kanidm_unixd ..."); + // Try to connect to the daemon. + match UnixStream::connect(&task_sock_path).await { + // Did we connect? + Ok(stream) => { + info!("Found kanidm_unixd, waiting for tasks ..."); + // Yep! Now let the main handler do it's job. + // If it returns (dc, etc, then we loop and try again). + handle_tasks(stream, &cfg.home_prefix).await; + } + Err(e) => { + error!("Unable to find kanidm_unixd, sleeping ..."); + debug!("\\---> {:?}", e); + // Back off. + time::sleep(Duration::from_millis(5000)).await; + } + } + } + }; + + server.await; +} diff --git a/kanidm_unix_int/src/unix_config.rs b/kanidm_unix_int/src/unix_config.rs index 4f66570f1..52d86cc59 100644 --- a/kanidm_unix_int/src/unix_config.rs +++ b/kanidm_unix_int/src/unix_config.rs @@ -1,6 +1,7 @@ use crate::constants::{ DEFAULT_CACHE_TIMEOUT, DEFAULT_CONN_TIMEOUT, DEFAULT_DB_PATH, DEFAULT_GID_ATTR_MAP, - DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, DEFAULT_SOCK_PATH, DEFAULT_UID_ATTR_MAP, + DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, DEFAULT_SOCK_PATH, + DEFAULT_TASK_SOCK_PATH, DEFAULT_UID_ATTR_MAP, }; use serde_derive::Deserialize; use std::fs::File; @@ -11,12 +12,14 @@ use std::path::Path; struct ConfigInt { db_path: Option, sock_path: Option, + task_sock_path: Option, conn_timeout: Option, cache_timeout: Option, pam_allowed_login_groups: Option>, default_shell: Option, home_prefix: Option, home_attr: Option, + home_alias: Option, uid_attr_map: Option, gid_attr_map: Option, } @@ -38,12 +41,14 @@ pub enum UidAttr { pub struct KanidmUnixdConfig { pub db_path: String, pub sock_path: String, + pub task_sock_path: String, pub conn_timeout: u64, pub cache_timeout: u64, pub pam_allowed_login_groups: Vec, pub default_shell: String, pub home_prefix: String, pub home_attr: HomeAttr, + pub home_alias: Option, pub uid_attr_map: UidAttr, pub gid_attr_map: UidAttr, } @@ -59,12 +64,14 @@ impl KanidmUnixdConfig { KanidmUnixdConfig { db_path: DEFAULT_DB_PATH.to_string(), sock_path: DEFAULT_SOCK_PATH.to_string(), + task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(), conn_timeout: DEFAULT_CONN_TIMEOUT, cache_timeout: DEFAULT_CACHE_TIMEOUT, pam_allowed_login_groups: Vec::new(), default_shell: DEFAULT_SHELL.to_string(), home_prefix: DEFAULT_HOME_PREFIX.to_string(), home_attr: DEFAULT_HOME_ATTR, + home_alias: DEFAULT_HOME_ALIAS, uid_attr_map: DEFAULT_UID_ATTR_MAP, gid_attr_map: DEFAULT_GID_ATTR_MAP, } @@ -93,6 +100,7 @@ impl KanidmUnixdConfig { Ok(KanidmUnixdConfig { db_path: config.db_path.unwrap_or(self.db_path), sock_path: config.sock_path.unwrap_or(self.sock_path), + task_sock_path: config.task_sock_path.unwrap_or(self.task_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 @@ -112,6 +120,19 @@ impl KanidmUnixdConfig { } }) .unwrap_or(self.home_attr), + home_alias: config + .home_alias + .and_then(|v| match v.as_str() { + "none" => Some(None), + "uuid" => Some(Some(HomeAttr::Uuid)), + "spn" => Some(Some(HomeAttr::Spn)), + "name" => Some(Some(HomeAttr::Name)), + _ => { + warn!("Invalid home_alias configured, using default ..."); + None + } + }) + .unwrap_or(self.home_alias), uid_attr_map: config .uid_attr_map .and_then(|v| match v.as_str() { diff --git a/kanidm_unix_int/src/unix_proto.rs b/kanidm_unix_int/src/unix_proto.rs index d88316ef7..8f0c1219b 100644 --- a/kanidm_unix_int/src/unix_proto.rs +++ b/kanidm_unix_int/src/unix_proto.rs @@ -25,6 +25,7 @@ pub enum ClientRequest { NssGroupByName(String), PamAuthenticate(String, String), PamAccountAllowed(String), + PamAccountBeginSession(String), InvalidateCache, ClearCache, Status, @@ -41,3 +42,21 @@ pub enum ClientResponse { Ok, Error, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HomeDirectoryInfo { + pub gid: u32, + pub name: String, + pub aliases: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TaskRequest { + HomeDirectory(HomeDirectoryInfo), +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum TaskResponse { + Success, + Error(String), +} diff --git a/kanidm_unix_int/tests/cache_layer_test.rs b/kanidm_unix_int/tests/cache_layer_test.rs index f1bd80ec9..20482737c 100644 --- a/kanidm_unix_int/tests/cache_layer_test.rs +++ b/kanidm_unix_int/tests/cache_layer_test.rs @@ -9,8 +9,8 @@ use kanidm::core::create_server_core; use kanidm_unix_common::cache::{CacheLayer, Id}; use kanidm_unix_common::constants::{ - DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_SHELL, - DEFAULT_UID_ATTR_MAP, + DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, + DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP, }; use tokio::runtime::Runtime; @@ -117,6 +117,7 @@ fn run_test(fix_fn: fn(&mut KanidmClient) -> (), test_fn: fn(CacheLayer, KanidmA DEFAULT_SHELL.to_string(), DEFAULT_HOME_PREFIX.to_string(), DEFAULT_HOME_ATTR, + DEFAULT_HOME_ALIAS, DEFAULT_UID_ATTR_MAP, DEFAULT_GID_ATTR_MAP, )) diff --git a/kanidmd/Cargo.toml b/kanidmd/Cargo.toml index 65cc6fcb6..bc9907196 100644 --- a/kanidmd/Cargo.toml +++ b/kanidmd/Cargo.toml @@ -73,17 +73,12 @@ sshkeys = "0.3" rpassword = "5.0" num_cpus = "1.10" -idlset = { version = "^0.1.12" , features = ["use_smallvec"] } -# idlset = { path = "../../idlset", features = ["use_smallvec"] } - zxcvbn = "2.0" base64 = "0.13" +idlset = { version = "^0.1.12" , features = ["use_smallvec"] } ldap3_server = "0.1" -# ldap3_server = { path = "../../ldap3_server" } - webauthn-rs = "0.3.0-alpha.5" -# webauthn-rs = { path = "../../webauthn-rs" } libc = "0.2" users = "0.11" @@ -98,7 +93,6 @@ simd_support = [ "concread/simd_support" ] criterion = { version = "0.3", features = ["html_reports"] } # For testing webauthn webauthn-authenticator-rs = "0.3.0-alpha.5" -# webauthn-authenticator-rs = { path = "../../webauthn-authenticator-rs" } [dev-dependencies.cargo-husky] version = "1" diff --git a/kanidmd/src/lib/value.rs b/kanidmd/src/lib/value.rs index b0931c57c..0f8337254 100644 --- a/kanidmd/src/lib/value.rs +++ b/kanidmd/src/lib/value.rs @@ -23,11 +23,12 @@ lazy_static! { }; static ref INAME_RE: Regex = { #[allow(clippy::expect_used)] - Regex::new("^(_.*|.*(\\s|@|,|=).*|\\d+|root|nobody|nogroup|wheel|sshd|shadow|systemd.*)$").expect("Invalid Iname regex found") - // ^ ^ ^ - // | | \- must not be only integers - // | \- must not contain whitespace, @, ',', = - // \- must not start with _ + Regex::new("^((\\.|_).*|.*(\\s|@|,|/|\\\\|=).*|\\d+|root|nobody|nogroup|wheel|sshd|shadow|systemd.*)$").expect("Invalid Iname regex found") + // ^ ^ ^ ^ + // | | | \- must not be a reserved name. + // | | \- must not be only integers + // | \- must not contain whitespace, @, ',', /, \, = + // \- must not start with _ or . // Them's be the rules. }; static ref NSUNIQUEID_RE: Regex = { @@ -1609,9 +1610,10 @@ mod tests { * - be a pure int (confusion to gid/uid/linux) * - a uuid (confuses our name mapper) * - contain an @ (confuses SPN) - * - can not start with _ (... I forgot but it's important I swear >.>) + * - can not start with _ (... api end points have _ as a magic char) * - can not have spaces (confuses too many systems :() * - can not have = or , (confuses ldap) + * - can not have ., /, \ (path injection attacks) */ let inv1 = Value::new_iname("1234"); let inv2 = Value::new_iname("bc23f637-4439-4c07-b95d-eaed0d9e4b8b");