Compare commits

...

9 commits

Author SHA1 Message Date
Firstyear 730ba1cdf5
Merge 71039cf069 into aee9ed05f3 2025-04-08 20:23:20 +02:00
Firstyear aee9ed05f3
Update fs4 and improve klock handling () 2025-04-08 05:04:26 +00:00
James Hodgkinson 5458b13398
Less footguns () 2025-04-08 04:48:53 +00:00
Firstyear 94b6287e27
Unify unix config parser ()
* Unify unix config parser
* Document the various structs
* Compiler Update
2025-04-08 14:21:26 +10:00
dependabot[bot] b6813a11d3
Bump openssl from 0.10.71 to 0.10.72 in the cargo group ()
Bumps the cargo group with 1 update: [openssl](https://github.com/sfackler/rust-openssl).


Updates `openssl` from 0.10.71 to 0.10.72
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: direct:production
  dependency-group: cargo
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 01:31:20 +00:00
William Brown 71039cf069 Updahte 2025-04-05 14:00:56 +10:00
William Brown d89fad3f42 Update 2025-04-05 13:55:53 +10:00
William Brown e35d4fddc1 Update 2025-04-05 13:55:53 +10:00
William Brown fa36325618 Ideas about service accounts 2025-04-05 13:55:53 +10:00
25 changed files with 838 additions and 635 deletions

18
Cargo.lock generated
View file

@ -1600,12 +1600,12 @@ dependencies = [
[[package]]
name = "fs4"
version = "0.12.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c29c30684418547d476f0b48e84f4821639119c483b1eccd566c8cd0cd05f521"
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
dependencies = [
"rustix 0.38.44",
"windows-sys 0.52.0",
"rustix 1.0.3",
"windows-sys 0.59.0",
]
[[package]]
@ -3064,6 +3064,8 @@ dependencies = [
"futures",
"kanidm_build_profiles",
"kanidm_proto",
"kanidm_utils_users",
"selinux",
"serde",
"serde_json",
"serde_with",
@ -3955,9 +3957,9 @@ checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
[[package]]
name = "openssl"
version = "0.10.71"
version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
@ -3987,9 +3989,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.106"
version = "0.9.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
dependencies = [
"cc",
"libc",

View file

@ -173,7 +173,7 @@ dhat = "0.3.3"
dyn-clone = "^1.0.17"
fernet = "^0.2.1"
filetime = "^0.2.24"
fs4 = "^0.12.0"
fs4 = "^0.13.0"
futures = "^0.3.31"
futures-util = { version = "^0.3.30", features = ["sink"] }
gix = { version = "0.64.0", default-features = false }
@ -210,7 +210,7 @@ notify-debouncer-full = { version = "0.5" }
num_enum = "^0.5.11"
oauth2_ext = { version = "^4.4.2", package = "oauth2", default-features = false }
openssl-sys = "^0.9"
openssl = "^0.10.70"
openssl = "^0.10.72"
opentelemetry = { version = "0.27.0" }
opentelemetry_api = { version = "0.27.0", features = ["logs", "metrics"] }

View file

@ -0,0 +1,171 @@
# Service Account Improvements - 2025
Initially when service accounts were added to Kanidm they were simply meant to be "detached"
accounts that could be used for some API access to Kani, or some other background tasks.
But as the server has evolved we need to consider how we can use these in other ways.
We have extented the OAuth2 client types to now almost act like a service account, especially
with the behaviour of things like a client credentials grant.
At this point we need to decide how to proceed with service accounts and what shape they could
take in the future.
## Prior Art
* (Microsoft AD-DS Service Accounts)[https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-service-accounts]
* (FreeIPA Service Principals)[https://www.freeipa.org/page/Administrators_Guide#managing-service-principals]
Note that both of these have some kerberos centric ideas as KRB requires service accounts to mutually
authenticate to clients, which means they need to maintain credentials. This is different to our needs,
but there are still some ideas in these docs worth knowing about and considering like group managed
service accounts (gMSA).
## Current state of affairs
We have:
* Break glass accounts (`admin`/`idm_admin`) are service accounts, may not have delegated management.
* OAuth2 is not a service account, supports delegated management.
* Service accounts can be group or user managed.
* Applications (To Be Introduced) is an extension of a Service account.
From this we can see that we have some separation, but also some cross over of functionality.
break glass isn't delegated, but service account is, OAuth2 isn't an SA, but Applications are.
## Capabilities
In order to properly handle this, we don't want to grant unbounded abilities to types, we don't
want to fully merge them, but we want to be able to mix-match what they require.
This also makes it possible in the future that we can more easily assign (or remove) a capability
from an account type.
To achieve this we should introduce the idea of capabilities - capabilities can act via schema
classes, and we can extend the schema such that only the parent class needs to know that the
capabilities class is required.
This allows us to nominate more carefully what each role type can or can't do. More importantly
within the server, we don't have to hardcode that "service accounts and applications" can use
api tokens vs every other capability type. We only need look for the capability on the entry.
| Capabilities | Api Token | OAuth2 Sessions | Interactive Login |
|-----------------|------------------|------------------------------|---------------------|
| OAuth2 | No | Via Client Credentials Grant | No |
| Application | Yes (ro) | No | No |
| Service Account | Yes (rw capable) | Yes (via session grant (TBD) | Yes (to be removed) |
| Machine Account | Yes (ro) | No | No |
| Break Glass | No | No | Yes |
| Person | No | Yes | Yes |
A key requirement of this is that we want each role to have a defined intent - it shouldn't be
the everything role, it still needs to be focused and administered in it's own right.
| | Intent |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| OAuth2 | An OAuth2 client (external server/service) that is treating Kani as the IDP it trusts to validate user authorisation to it's resources. |
| Application | An application password context, allowing per-user/per-device/per-application passwords to validated, as well as defining group based authorisation of whom may use this application |
| Service Account | An account that belongs to a process or automation that needs to read from or write to Kanidm, or a Kanidm related service. |
| Machine Account | A domain joined machine that is reads user posix or login information. May be used to configure machine service accounts in future. |
| Break Glass | An emergency access account used in disaster recovery. |
| Person | A humans owned account that needs to authenticate day to day, and self manage their own credentials. A person may need to manage other accounts and resource types |
This has the benefit that it makes it easier to assign the permissions via ACP (since we can filter
on the Target class *and* capability type).
### Example
An Email service has an SMTP gateway and OAuth2 web ui.
Although this is "the email service" it is made up of multiple parts that each have their own intents.
The Webui has an Oauth2 client created to define the relationship of who may access the webui.
An LDAP application is made to allow IMAP/SMTP processes to authenticate users with application passwords and
to read users PII via LDAP.
## Below was the drafting process of some ideas
### Attach roles to service accounts.
In this approach we centre the service account, and allow optional extension of other concerns. This
would make OAuth2 applications an extension of a service account. Similar application would become
an extension of service account.
This would mean that we create a service account first, then need a way to extend it with the
application or oauth2 types.
This would mean that a service account could potentially be everything - an application password
provider, an oauth2 client, and more. This would make the administration very difficult and deeply
nested on the single service account type, and could encourage bad administration practices as
admins can "shovel in" every possible role to single accounts.
PROS:
* OAuth2 applications get the ability to have api tokens to kani for other functionality
* Fullstacks like a mail server get a single SA that does everything
* These whole stack service accounts get access to every auth type and feature available
CONS:
* Makes the API around service accounts a bit messier
* Compromise of the SA or SA Manager may lead to higher impact due to more features in one place
* May be confusing to administrators
* More "inheritance" of schema classes, when we may want to try to simplify to single classes in line with SCIM.
* Harder to audit capabilities
* The administration UI becomes a shitshow as the Service Account is now a kitchen sink.
### Separate Concerns
In this approach we split our concerns. This is similar to today, but taken a bit further.
In this example, we would split Application to *just* be about the concern of an authentication
domain for LDAP applications. OAuth2 stays as *just* a configuration of the client and it's behaviour.
We would change the break glass accounts to be a separate type to Service Account. Service Account
becomes closer to the concept of a pure api access account. The break glass accounts become a
dedicated "emergency access account" type.
PROS:
* Similar to today, only small cleanup needed
* Separation of concerns and credentials limit's blast radius of a possible compromise.
* Easier auditing of capabilities of each account
CONS:
* More administrative overhead to manage the multiple accounts
* Stacked applications will need mulitple configurations for a role - OAuth2, LDAP application, Service accounts for example in an email server with a WebUI.
### Bit of A, bit of B, cleanup
AKA Capabilities
Rather than fully merge all the types, or fully split them, have a *little* merge of some bits, allowing
some limited extension of actions to specific actors. Effectively we end up granting *capabilities*
to different roles, and we can add extra capabilities later if we want.
OAuth2 and Applications would gain the ability to have API tokens associated for some tasks and
could act on Kanidm, but they wouldn't be fully fleshed service accounts.
| Capabilities | Api Token | OAuth2 Sessions | Interactive Login |
|-----------------|------------------|------------------------------|---------------------|
| OAuth2 | No | Via Client Credentials Grant | No |
| Application | Yes | No | No |
| Service Account | Yes (rw capable) | Yes (via session grant (TBD) | Yes (to be removed) |
| Break Glass | No | No | Yes |
PROS:
* Minimises changes to existing deployments
* Grants some new abilities within limits to other roles
* While not as locked down as separate concern proposal, still minimises the risk of compromise of an SA
CONS:
* Requires admins to have multiple accounts in some contexts (as above).
* Auditing requires knowledge of what each roles capabilities are, and what the capabilities do

View file

@ -8,7 +8,7 @@ const MD5_TRANSPOSE: &[u8] = b"\x0c\x06\x00\x0d\x07\x01\x0e\x08\x02\x0f\x09\x03\
const CRYPT_HASH64: &[u8] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
pub fn md5_sha2_hash64_encode(bs: &[u8]) -> String {
let ngroups = (bs.len() + 2) / 3;
let ngroups = bs.len().div_ceil(3);
let mut out = String::with_capacity(ngroups * 4);
for g in 0..ngroups {
let mut g_idx = g * 3;

View file

@ -21,6 +21,8 @@ ${SUDOCMD} apt-get update &&
cmake \
build-essential \
jq \
lld \
clang \
tpm-udev
if [ -z "${PACKAGING}" ]; then
@ -73,10 +75,6 @@ if [ -z "$(which cargo)" ]; then
ERROR=1
fi
if [ $ERROR -eq 0 ] && [ -z "$(which cross)" ]; then
echo "You don't have cross installed! Installing it now..."
cargo install -f cross
fi
if [ $ERROR -eq 0 ] && [ -z "$(which cargo-deb)" ]; then
echo "You don't have cargo-deb installed! Installing it now..."
cargo install -f cargo-deb

View file

@ -465,13 +465,13 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
return ExitCode::FAILURE;
}
match &opt.commands {
let lock_was_setup = match &opt.commands {
// we aren't going to touch the DB so we can carry on
KanidmdOpt::ShowReplicationCertificate { .. }
| KanidmdOpt::RenewReplicationCertificate { .. }
| KanidmdOpt::RefreshReplicationConsumer { .. }
| KanidmdOpt::RecoverAccount { .. }
| KanidmdOpt::HealthCheck(_) => (),
| KanidmdOpt::HealthCheck(_) => None,
_ => {
// Okay - Lets now create our lock and go.
#[allow(clippy::expect_used)]
@ -482,24 +482,53 @@ async fn start_daemon(opt: KanidmdParser, config: Configuration) -> ExitCode {
let flock = match File::create(&klock_path) {
Ok(flock) => flock,
Err(e) => {
error!("ERROR: Refusing to start - unable to create kanidmd exclusive lock at {} - {:?}", klock_path.display(), e);
Err(err) => {
error!(
"ERROR: Refusing to start - unable to create kanidmd exclusive lock at {}",
klock_path.display()
);
error!(?err);
return ExitCode::FAILURE;
}
};
match flock.try_lock_exclusive() {
Ok(()) => debug!("Acquired kanidm exclusive lock"),
Err(e) => {
error!("ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {} - {:?}", klock_path.display(), e);
Ok(true) => debug!("Acquired kanidm exclusive lock"),
Ok(false) => {
error!(
"ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
klock_path.display()
);
error!("Is another kanidmd process running?");
return ExitCode::FAILURE;
}
Err(err) => {
error!(
"ERROR: Refusing to start - unable to lock kanidmd exclusive lock at {}",
klock_path.display()
);
error!(?err);
return ExitCode::FAILURE;
}
};
Some(klock_path)
}
};
let result_code = kanidm_main(config, opt).await;
if let Some(klock_path) = lock_was_setup {
if let Err(reason) = std::fs::remove_file(&klock_path) {
warn!(
?reason,
"WARNING: Unable to clean up kanidmd exclusive lock at {}",
klock_path.display()
);
}
}
kanidm_main(config, opt).await
result_code
}
fn main() -> ExitCode {

View file

@ -165,10 +165,10 @@ impl ReplEntryV1 {
// but for now, if it's an empty set in any capacity, we map
// to None and just send the Cid since they have the same result
// on how the entry/attr state looks at each end.
if maybe.len() > 0 {
Some(maybe.to_db_valueset_v2())
} else {
if maybe.is_empty() {
None
} else {
Some(maybe.to_db_valueset_v2())
}
);
@ -298,10 +298,10 @@ impl ReplIncrementalEntryV1 {
let live_attr = live_attrs.get(attr_name);
let cid = cid.into();
let attr = live_attr.and_then(|maybe| {
if maybe.len() > 0 {
Some(maybe.to_db_valueset_v2())
} else {
if maybe.is_empty() {
None
} else {
Some(maybe.to_db_valueset_v2())
}
});

View file

@ -83,7 +83,7 @@ impl ActorReader {
// Is this a design flaw? We probably need to know what the state was that we
// requested to move to?
match (&self.state, action, result) {
(State::Unauthenticated { .. }, TransitionAction::Login, TransitionResult::Ok) => {
(State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => {
self.state = State::Authenticated;
}
(State::Authenticated, TransitionAction::ReadSelfMemberOf, TransitionResult::Ok) => {

View file

@ -14,6 +14,8 @@ repository = { workspace = true }
[features]
default = ["unix"]
unix = []
selinux = ["dep:selinux"]
tpm = []
[lib]
name = "kanidm_unix_common"
@ -35,6 +37,11 @@ tokio-util = { workspace = true, features = ["codec"] }
toml = { workspace = true }
tracing = { workspace = true }
selinux = { workspace = true, optional = true }
[target.'cfg(not(target_family = "windows"))'.dependencies]
kanidm_utils_users = { workspace = true }
[build-dependencies]
kanidm_build_profiles = { workspace = true }

View file

@ -26,3 +26,6 @@ pub mod unix_config;
pub mod unix_passwd;
#[cfg(target_family = "unix")]
pub mod unix_proto;
#[cfg(all(target_family = "unix", feature = "selinux"))]
pub mod selinux_util;

View file

@ -1,13 +1,24 @@
//! This is configuration definitions and parser for the various unix integration
//! tools and services. This needs to support a number of use cases like pam/nss
//! modules parsing the config quickly and the unix daemon which has to connect to
//! various backend sources.
//!
//! To achieve this the configuration has two main sections - the configuration
//! specification which will be parsed by the tools, then the configuration as
//! relevant to that tool.
use std::env;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{ErrorKind, Read};
use std::path::Path;
use std::path::{Path, PathBuf};
#[cfg(all(target_family = "unix", feature = "selinux"))]
use crate::selinux_util;
use crate::unix_passwd::UnixIntegrationError;
use serde::Deserialize;
use crate::constants::*;
use serde::Deserialize;
#[derive(Debug, Copy, Clone)]
pub enum HomeAttr {
@ -49,35 +60,539 @@ impl Display for UidAttr {
}
}
#[derive(Debug, Clone, Default)]
pub enum HsmType {
#[cfg_attr(not(feature = "tpm"), default)]
Soft,
#[cfg_attr(feature = "tpm", default)]
TpmIfPossible,
Tpm,
}
impl Display for HsmType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
HsmType::Soft => write!(f, "Soft"),
HsmType::TpmIfPossible => write!(f, "Tpm if possible"),
HsmType::Tpm => write!(f, "Tpm"),
}
}
}
// Allowed as the large enum is only short lived at startup to the true config
#[allow(clippy::large_enum_variant)]
// This bit of magic lets us deserialise the old config and the new versions.
#[derive(Debug, Deserialize)]
struct ConfigInt {
#[serde(untagged)]
enum ConfigUntagged {
Versioned(ConfigVersion),
Legacy(ConfigInt),
}
#[derive(Debug, Deserialize)]
#[serde(tag = "version")]
enum ConfigVersion {
#[serde(rename = "2")]
V2 {
#[serde(flatten)]
values: ConfigV2,
},
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
/// This is the version 2 of the JSON configuration specification for the unixd suite.
struct ConfigV2 {
cache_db_path: Option<String>,
sock_path: Option<String>,
task_sock_path: Option<String>,
cache_timeout: Option<u64>,
default_shell: Option<String>,
home_prefix: Option<String>,
home_mount_prefix: Option<String>,
home_attr: Option<String>,
home_alias: Option<String>,
use_etc_skel: Option<bool>,
uid_attr_map: Option<String>,
gid_attr_map: Option<String>,
selinux: Option<bool>,
hsm_pin_path: Option<String>,
hsm_type: Option<String>,
tpm_tcti_name: Option<String>,
kanidm: Option<KanidmConfigV2>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GroupMap {
pub local: String,
pub with: String,
}
#[derive(Debug, Deserialize)]
struct KanidmConfigV2 {
conn_timeout: Option<u64>,
request_timeout: Option<u64>,
pam_allowed_login_groups: Option<Vec<String>>,
#[serde(default)]
map_group: Vec<GroupMap>,
}
#[derive(Debug, Deserialize)]
/// This is the version 1 of the JSON configuration specification for the unixd suite.
struct ConfigInt {
db_path: Option<String>,
sock_path: Option<String>,
task_sock_path: Option<String>,
conn_timeout: Option<u64>,
request_timeout: Option<u64>,
cache_timeout: Option<u64>,
pam_allowed_login_groups: Option<Vec<String>>,
default_shell: Option<String>,
home_prefix: Option<String>,
home_mount_prefix: Option<String>,
home_attr: Option<String>,
home_alias: Option<String>,
use_etc_skel: Option<bool>,
uid_attr_map: Option<String>,
gid_attr_map: Option<String>,
selinux: Option<bool>,
#[serde(default)]
allow_local_account_override: Vec<String>,
hsm_pin_path: Option<String>,
hsm_type: Option<String>,
tpm_tcti_name: Option<String>,
// Detect and warn on values in these places - this is to catch
// when someone is using a v2 value on a v1 config.
#[serde(default)]
cache_db_path: Option<toml::value::Value>,
#[serde(default)]
kanidm: Option<toml::value::Value>,
}
// ========================================================================
#[derive(Debug)]
/// This is the parsed Kanidm provider configuration that the Unixd resolver
/// will use to connect to Kanidm.
pub struct KanidmConfig {
pub conn_timeout: u64,
pub request_timeout: u64,
pub pam_allowed_login_groups: Vec<String>,
pub map_group: Vec<GroupMap>,
}
#[derive(Debug)]
pub struct KanidmUnixdConfig {
/// This is the parsed configuration for the Unixd resolver.
pub struct UnixdConfig {
pub cache_db_path: String,
pub sock_path: String,
pub task_sock_path: String,
pub cache_timeout: u64,
pub unix_sock_timeout: u64,
pub default_shell: String,
pub home_prefix: PathBuf,
pub home_mount_prefix: Option<PathBuf>,
pub home_attr: HomeAttr,
pub home_alias: Option<HomeAttr>,
pub use_etc_skel: bool,
pub uid_attr_map: UidAttr,
pub gid_attr_map: UidAttr,
pub selinux: bool,
pub hsm_type: HsmType,
pub hsm_pin_path: String,
pub tpm_tcti_name: String,
pub kanidm_config: Option<KanidmConfig>,
}
impl Default for UnixdConfig {
fn default() -> Self {
UnixdConfig::new()
}
}
impl Display for UnixdConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "cache_db_path: {}", &self.cache_db_path)?;
writeln!(f, "sock_path: {}", self.sock_path)?;
writeln!(f, "task_sock_path: {}", self.task_sock_path)?;
writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)?;
writeln!(f, "cache_timeout: {}", self.cache_timeout)?;
writeln!(f, "default_shell: {}", self.default_shell)?;
writeln!(f, "home_prefix: {:?}", self.home_prefix)?;
match self.home_mount_prefix.as_deref() {
Some(val) => writeln!(f, "home_mount_prefix: {:?}", val)?,
None => writeln!(f, "home_mount_prefix: unset")?,
}
writeln!(f, "home_attr: {}", self.home_attr)?;
match self.home_alias {
Some(val) => writeln!(f, "home_alias: {}", val)?,
None => writeln!(f, "home_alias: unset")?,
}
writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?;
writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?;
writeln!(f, "hsm_type: {}", self.hsm_type)?;
writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?;
writeln!(f, "selinux: {}", self.selinux)?;
if let Some(kconfig) = &self.kanidm_config {
writeln!(f, "kanidm: enabled")?;
writeln!(
f,
"kanidm pam_allowed_login_groups: {:#?}",
kconfig.pam_allowed_login_groups
)?;
writeln!(f, "kanidm conn_timeout: {}", kconfig.conn_timeout)?;
writeln!(f, "kanidm request_timeout: {}", kconfig.request_timeout)?;
} else {
writeln!(f, "kanidm: disabled")?;
};
Ok(())
}
}
impl UnixdConfig {
pub fn new() -> Self {
let cache_db_path = match env::var("KANIDM_CACHE_DB_PATH") {
Ok(val) => val,
Err(_) => DEFAULT_CACHE_DB_PATH.into(),
};
let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") {
Ok(val) => val,
Err(_) => DEFAULT_HSM_PIN_PATH.into(),
};
UnixdConfig {
cache_db_path,
sock_path: DEFAULT_SOCK_PATH.to_string(),
task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: DEFAULT_CACHE_TIMEOUT,
default_shell: DEFAULT_SHELL.to_string(),
home_prefix: DEFAULT_HOME_PREFIX.into(),
home_mount_prefix: None,
home_attr: DEFAULT_HOME_ATTR,
home_alias: DEFAULT_HOME_ALIAS,
use_etc_skel: DEFAULT_USE_ETC_SKEL,
uid_attr_map: DEFAULT_UID_ATTR_MAP,
gid_attr_map: DEFAULT_GID_ATTR_MAP,
selinux: DEFAULT_SELINUX,
hsm_pin_path,
hsm_type: HsmType::default(),
tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(),
kanidm_config: None,
}
}
pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
self,
config_path: P,
) -> Result<Self, UnixIntegrationError> {
debug!("Attempting to load configuration from {:#?}", &config_path);
let mut f = match File::open(&config_path) {
Ok(f) => {
debug!("Successfully opened configuration file {:#?}", &config_path);
f
}
Err(e) => {
match e.kind() {
ErrorKind::NotFound => {
debug!(
"Configuration file {:#?} not found, skipping.",
&config_path
);
}
ErrorKind::PermissionDenied => {
warn!(
"Permission denied loading configuration file {:#?}, skipping.",
&config_path
);
}
_ => {
debug!(
"Unable to open config file {:#?} [{:?}], skipping ...",
&config_path, e
);
}
};
return Ok(self);
}
};
let mut contents = String::new();
f.read_to_string(&mut contents).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
match config {
ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config),
ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => {
self.apply_from_config_v2(values)
}
}
}
fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> {
if config.kanidm.is_some() || config.cache_db_path.is_some() {
error!("You are using version=\"2\" options in a legacy config. THESE WILL NOT WORK.");
return Err(UnixIntegrationError);
}
let map_group = config
.allow_local_account_override
.iter()
.map(|name| GroupMap {
local: name.clone(),
with: name.clone(),
})
.collect();
let kanidm_config = Some(KanidmConfig {
conn_timeout: config.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
request_timeout: config.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
pam_allowed_login_groups: config.pam_allowed_login_groups.unwrap_or_default(),
map_group,
});
// Now map the values into our config.
Ok(UnixdConfig {
cache_db_path: config.db_path.unwrap_or(self.cache_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),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
default_shell: config.default_shell.unwrap_or(self.default_shell),
home_prefix: config
.home_prefix
.map(|p| p.into())
.unwrap_or(self.home_prefix.clone()),
home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
home_attr: config
.home_attr
.and_then(|v| match v.as_str() {
"uuid" => Some(HomeAttr::Uuid),
"spn" => Some(HomeAttr::Spn),
"name" => Some(HomeAttr::Name),
_ => {
warn!("Invalid home_attr configured, using default ...");
None
}
})
.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),
use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
uid_attr_map: config
.uid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid uid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.uid_attr_map),
gid_attr_map: config
.gid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid gid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.gid_attr_map),
selinux: match config.selinux.unwrap_or(self.selinux) {
#[cfg(all(target_family = "unix", feature = "selinux"))]
true => selinux_util::supported(),
_ => false,
},
hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
hsm_type: config
.hsm_type
.and_then(|v| match v.as_str() {
"soft" => Some(HsmType::Soft),
"tpm_if_possible" => Some(HsmType::TpmIfPossible),
"tpm" => Some(HsmType::Tpm),
_ => {
warn!("Invalid hsm_type configured, using default ...");
None
}
})
.unwrap_or(self.hsm_type),
tpm_tcti_name: config
.tpm_tcti_name
.unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
kanidm_config,
})
}
fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> {
let kanidm_config = if let Some(kconfig) = config.kanidm {
match &kconfig.pam_allowed_login_groups {
None => {
error!("You have a 'kanidm' section in the config but an empty pam_allowed_login_groups set. USERS CANNOT AUTH.")
}
Some(groups) => {
if groups.is_empty() {
error!("You have a 'kanidm' section in the config but an empty pam_allowed_login_groups set. USERS CANNOT AUTH.");
}
}
}
Some(KanidmConfig {
conn_timeout: kconfig.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
request_timeout: kconfig.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
pam_allowed_login_groups: kconfig.pam_allowed_login_groups.unwrap_or_default(),
map_group: kconfig.map_group,
})
} else {
error!(
"You are using a version 2 config without a 'kanidm' section. USERS CANNOT AUTH."
);
None
};
// Now map the values into our config.
Ok(UnixdConfig {
cache_db_path: config.cache_db_path.unwrap_or(self.cache_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),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
default_shell: config.default_shell.unwrap_or(self.default_shell),
home_prefix: config
.home_prefix
.map(|p| p.into())
.unwrap_or(self.home_prefix.clone()),
home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
home_attr: config
.home_attr
.and_then(|v| match v.as_str() {
"uuid" => Some(HomeAttr::Uuid),
"spn" => Some(HomeAttr::Spn),
"name" => Some(HomeAttr::Name),
_ => {
warn!("Invalid home_attr configured, using default ...");
None
}
})
.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),
use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
uid_attr_map: config
.uid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid uid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.uid_attr_map),
gid_attr_map: config
.gid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid gid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.gid_attr_map),
selinux: match config.selinux.unwrap_or(self.selinux) {
#[cfg(all(target_family = "unix", feature = "selinux"))]
true => selinux_util::supported(),
_ => false,
},
hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
hsm_type: config
.hsm_type
.and_then(|v| match v.as_str() {
"soft" => Some(HsmType::Soft),
"tpm_if_possible" => Some(HsmType::TpmIfPossible),
"tpm" => Some(HsmType::Tpm),
_ => {
warn!("Invalid hsm_type configured, using default ...");
None
}
})
.unwrap_or(self.hsm_type),
tpm_tcti_name: config
.tpm_tcti_name
.unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
kanidm_config,
})
}
}
#[derive(Debug)]
/// This is the parsed configuration that will be used by pam/nss tools that need fast access to
/// only the socket and timeout information related to the resolver.
pub struct PamNssConfig {
pub sock_path: String,
// pub conn_timeout: u64,
pub unix_sock_timeout: u64,
}
impl Default for KanidmUnixdConfig {
impl Default for PamNssConfig {
fn default() -> Self {
KanidmUnixdConfig::new()
PamNssConfig::new()
}
}
impl Display for KanidmUnixdConfig {
impl Display for PamNssConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "sock_path: {}", self.sock_path)?;
writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)
}
}
impl KanidmUnixdConfig {
impl PamNssConfig {
pub fn new() -> Self {
KanidmUnixdConfig {
PamNssConfig {
sock_path: DEFAULT_SOCK_PATH.to_string(),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
}
@ -124,22 +639,45 @@ impl KanidmUnixdConfig {
UnixIntegrationError
})?;
let config: ConfigInt = toml::from_str(contents.as_str()).map_err(|e| {
let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
match config {
ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config),
ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => {
self.apply_from_config_v2(values)
}
}
}
fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> {
let unix_sock_timeout = config
.conn_timeout
.map(|v| v * 2)
.unwrap_or(self.unix_sock_timeout);
// Now map the values into our config.
Ok(KanidmUnixdConfig {
Ok(PamNssConfig {
sock_path: config.sock_path.unwrap_or(self.sock_path),
unix_sock_timeout,
})
}
fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> {
let kanidm_conn_timeout = config
.kanidm
.as_ref()
.and_then(|k_config| k_config.conn_timeout)
.map(|timeout| timeout * 2);
// Now map the values into our config.
Ok(PamNssConfig {
sock_path: config.sock_path.unwrap_or(self.sock_path),
unix_sock_timeout: kanidm_conn_timeout.unwrap_or(self.unix_sock_timeout),
})
}
}
#[cfg(test)]
@ -165,9 +703,12 @@ mod tests {
if filename.starts_with("unixd") {
print!("Checking that {} parses as a valid config...", filename);
KanidmUnixdConfig::new()
UnixdConfig::new()
.read_options_from_optional_config(file.path())
.expect("Failed to parse");
.inspect_err(|e| {
println!("Failed to parse: {:?}", e);
})
.expect("Failed to parse!");
println!("OK");
}
}

View file

@ -1,5 +1,5 @@
use kanidm_unix_common::client_sync::DaemonClientBlocking;
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_config::PamNssConfig;
use kanidm_unix_common::unix_passwd::{
read_etc_group_file, read_etc_passwd_file, EtcGroup, EtcUser,
};
@ -36,7 +36,7 @@ impl RequestOptions {
fn connect_to_daemon(self) -> Source {
match self {
RequestOptions::Main { config_path } => {
let maybe_client = KanidmUnixdConfig::new()
let maybe_client = PamNssConfig::new()
.read_options_from_optional_config(config_path)
.ok()
.and_then(|cfg| {

View file

@ -2,7 +2,7 @@ use crate::constants::PamResultCode;
use crate::module::PamResult;
use crate::pam::ModuleOptions;
use kanidm_unix_common::client_sync::DaemonClientBlocking;
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_config::PamNssConfig;
use kanidm_unix_common::unix_passwd::{
read_etc_passwd_file, read_etc_shadow_file, EtcShadow, EtcUser,
};
@ -44,7 +44,7 @@ impl RequestOptions {
fn connect_to_daemon(self) -> Source {
match self {
RequestOptions::Main { config_path } => {
let maybe_client = KanidmUnixdConfig::new()
let maybe_client = PamNssConfig::new()
.read_options_from_optional_config(config_path)
.ok()
.and_then(|cfg| {

View file

@ -36,7 +36,7 @@ use std::convert::TryFrom;
use std::ffi::CStr;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_config::PamNssConfig;
use crate::core::{self, RequestOptions};
use crate::pam::constants::*;
@ -50,8 +50,8 @@ use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*;
pub fn get_cfg() -> Result<KanidmUnixdConfig, PamResultCode> {
KanidmUnixdConfig::new()
pub fn get_cfg() -> Result<PamNssConfig, PamResultCode> {
PamNssConfig::new()
.read_options_from_optional_config(DEFAULT_CONFIG_PATH)
.map_err(|_| PamResultCode::PAM_SERVICE_ERR)
}

View file

@ -14,8 +14,8 @@ repository = { workspace = true }
[features]
default = ["unix"]
unix = []
selinux = ["dep:selinux"]
tpm = ["kanidm-hsm-crypto/tpm"]
selinux = ["dep:selinux", "kanidm_unix_common/selinux"]
tpm = ["kanidm-hsm-crypto/tpm", "kanidm_unix_common/tpm"]
[[bin]]
name = "kanidm_unixd"

View file

@ -18,7 +18,7 @@ use std::process::ExitCode;
use clap::Parser;
use kanidm_unix_common::client::DaemonClient;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_config::PamNssConfig;
use kanidm_unix_common::unix_proto::{
ClientRequest, ClientResponse, PamAuthRequest, PamAuthResponse, PamServiceInfo,
};
@ -28,8 +28,7 @@ include!("../opt/tool.rs");
macro_rules! setup_client {
() => {{
let Ok(cfg) =
KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
let Ok(cfg) = PamNssConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
else {
error!("Failed to parse {}", DEFAULT_CONFIG_PATH);
return ExitCode::FAILURE;

View file

@ -19,7 +19,7 @@ use std::process::ExitCode;
use clap::Parser;
use kanidm_unix_common::client::DaemonClient;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
use kanidm_unix_common::unix_config::PamNssConfig;
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
include!("../opt/ssh_authorizedkeys.rs");
@ -44,8 +44,7 @@ async fn main() -> ExitCode {
debug!("Starting authorized keys tool ...");
let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH)
{
let cfg = match PamNssConfig::new().read_options_from_optional_config(DEFAULT_CONFIG_PATH) {
Ok(c) => c,
Err(e) => {
error!("Failed to parse {}: {:?}", DEFAULT_CONFIG_PATH, e);

View file

@ -18,6 +18,7 @@ use kanidm_hsm_crypto::{soft::SoftTpm, AuthValue, BoxedDynTpm, Tpm};
use kanidm_proto::constants::DEFAULT_CLIENT_CONFIG_PATH;
use kanidm_proto::internal::OperationError;
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::{HsmType, UnixdConfig};
use kanidm_unix_common::unix_passwd::EtcDb;
use kanidm_unix_common::unix_proto::{
ClientRequest, ClientResponse, TaskRequest, TaskRequestFrame, TaskResponse,
@ -27,7 +28,6 @@ use kanidm_unix_resolver::idprovider::interface::IdProvider;
use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider;
use kanidm_unix_resolver::idprovider::system::SystemProvider;
use kanidm_unix_resolver::resolver::Resolver;
use kanidm_unix_resolver::unix_config::{HsmType, UnixdConfig};
use kanidm_utils_users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
use libc::umask;
use sketching::tracing::span;

View file

@ -13,11 +13,11 @@
use bytes::{BufMut, BytesMut};
use futures::{SinkExt, StreamExt};
use kanidm_unix_common::constants::DEFAULT_CONFIG_PATH;
use kanidm_unix_common::unix_config::UnixdConfig;
use kanidm_unix_common::unix_passwd::{parse_etc_group, parse_etc_passwd, parse_etc_shadow, EtcDb};
use kanidm_unix_common::unix_proto::{
HomeDirectoryInfo, TaskRequest, TaskRequestFrame, TaskResponse,
};
use kanidm_unix_resolver::unix_config::UnixdConfig;
use kanidm_utils_users::{get_effective_gid, get_effective_uid};
use libc::{lchown, umask};
use notify_debouncer_full::notify::RecommendedWatcher;
@ -43,7 +43,7 @@ use tokio_util::codec::{Decoder, Encoder, Framed};
use walkdir::WalkDir;
#[cfg(all(target_family = "unix", feature = "selinux"))]
use kanidm_unix_resolver::selinux_util;
use kanidm_unix_common::selinux_util;
struct TaskCodec;

View file

@ -1,24 +1,22 @@
use crate::db::KeyStoreTxn;
use crate::unix_config::{GroupMap, KanidmConfig};
use async_trait::async_trait;
use hashbrown::HashMap;
use kanidm_client::{ClientError, KanidmClient, StatusCode};
use kanidm_proto::internal::OperationError;
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
use std::collections::BTreeSet;
use std::time::{Duration, SystemTime};
use tokio::sync::{broadcast, Mutex};
use kanidm_lib_crypto::CryptoPolicy;
use kanidm_lib_crypto::DbPasswordV1;
use kanidm_lib_crypto::Password;
use super::interface::{
tpm::{self, HmacKey, Tpm},
AuthCredHandler, AuthRequest, AuthResult, GroupToken, GroupTokenState, Id, IdProvider,
IdpError, ProviderOrigin, UserToken, UserTokenState,
};
use crate::db::KeyStoreTxn;
use async_trait::async_trait;
use hashbrown::HashMap;
use kanidm_client::{ClientError, KanidmClient, StatusCode};
use kanidm_lib_crypto::CryptoPolicy;
use kanidm_lib_crypto::DbPasswordV1;
use kanidm_lib_crypto::Password;
use kanidm_proto::internal::OperationError;
use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
use kanidm_unix_common::unix_config::{GroupMap, KanidmConfig};
use kanidm_unix_common::unix_proto::PamAuthRequest;
use std::collections::BTreeSet;
use std::time::{Duration, SystemTime};
use tokio::sync::{broadcast, Mutex};
const KANIDM_HMAC_KEY: &str = "kanidm-hmac-key";
const KANIDM_PWV1_KEY: &str = "kanidm-pw-v1";

View file

@ -23,7 +23,3 @@ pub mod db;
pub mod idprovider;
#[cfg(target_family = "unix")]
pub mod resolver;
#[cfg(all(target_family = "unix", feature = "selinux"))]
pub mod selinux_util;
#[cfg(target_family = "unix")]
pub mod unix_config;

View file

@ -1,18 +1,4 @@
// use async_trait::async_trait;
use hashbrown::HashMap;
use std::fmt::Display;
use std::num::NonZeroUsize;
use std::ops::DerefMut;
use std::path::{Path, PathBuf};
use std::string::ToString;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use lru::LruCache;
use time::OffsetDateTime;
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::db::{Cache, Db};
use crate::idprovider::interface::{
AuthCredHandler,
@ -30,13 +16,25 @@ use crate::idprovider::interface::{
use crate::idprovider::system::{
Shadow, SystemAuthResult, SystemProvider, SystemProviderAuthInit, SystemProviderSession,
};
use crate::unix_config::{HomeAttr, UidAttr};
use hashbrown::HashMap;
use kanidm_unix_common::constants::DEFAULT_SHELL_SEARCH_PATHS;
use kanidm_unix_common::unix_config::{HomeAttr, UidAttr};
use kanidm_unix_common::unix_passwd::{EtcGroup, EtcShadow, EtcUser};
use kanidm_unix_common::unix_proto::{
HomeDirectoryInfo, NssGroup, NssUser, PamAuthRequest, PamAuthResponse, PamServiceInfo,
ProviderStatus,
};
use lru::LruCache;
use std::fmt::Display;
use std::num::NonZeroUsize;
use std::ops::DerefMut;
use std::path::{Path, PathBuf};
use std::string::ToString;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use time::OffsetDateTime;
use tokio::sync::Mutex;
use uuid::Uuid;
use kanidm_hsm_crypto::BoxedDynTpm;

View file

@ -1,538 +0,0 @@
use std::env;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{ErrorKind, Read};
use std::path::{Path, PathBuf};
#[cfg(all(target_family = "unix", feature = "selinux"))]
use crate::selinux_util;
use kanidm_unix_common::unix_passwd::UnixIntegrationError;
pub(crate) use kanidm_unix_common::unix_config::{HomeAttr, UidAttr};
use serde::Deserialize;
use kanidm_unix_common::constants::*;
// Allowed as the large enum is only short lived at startup to the true config
#[allow(clippy::large_enum_variant)]
// This bit of magic lets us deserialise the old config and the new versions.
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum ConfigUntagged {
Versioned(ConfigVersion),
Legacy(ConfigInt),
}
#[derive(Debug, Deserialize)]
#[serde(tag = "version")]
enum ConfigVersion {
#[serde(rename = "2")]
V2 {
#[serde(flatten)]
values: ConfigV2,
},
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigV2 {
cache_db_path: Option<String>,
sock_path: Option<String>,
task_sock_path: Option<String>,
cache_timeout: Option<u64>,
default_shell: Option<String>,
home_prefix: Option<String>,
home_mount_prefix: Option<String>,
home_attr: Option<String>,
home_alias: Option<String>,
use_etc_skel: Option<bool>,
uid_attr_map: Option<String>,
gid_attr_map: Option<String>,
selinux: Option<bool>,
hsm_pin_path: Option<String>,
hsm_type: Option<String>,
tpm_tcti_name: Option<String>,
kanidm: Option<KanidmConfigV2>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct GroupMap {
pub local: String,
pub with: String,
}
#[derive(Debug, Deserialize)]
struct KanidmConfigV2 {
conn_timeout: Option<u64>,
request_timeout: Option<u64>,
pam_allowed_login_groups: Option<Vec<String>>,
#[serde(default)]
map_group: Vec<GroupMap>,
}
#[derive(Debug, Deserialize)]
struct ConfigInt {
db_path: Option<String>,
sock_path: Option<String>,
task_sock_path: Option<String>,
conn_timeout: Option<u64>,
request_timeout: Option<u64>,
cache_timeout: Option<u64>,
pam_allowed_login_groups: Option<Vec<String>>,
default_shell: Option<String>,
home_prefix: Option<String>,
home_mount_prefix: Option<String>,
home_attr: Option<String>,
home_alias: Option<String>,
use_etc_skel: Option<bool>,
uid_attr_map: Option<String>,
gid_attr_map: Option<String>,
selinux: Option<bool>,
#[serde(default)]
allow_local_account_override: Vec<String>,
hsm_pin_path: Option<String>,
hsm_type: Option<String>,
tpm_tcti_name: Option<String>,
// Detect and warn on values in these places.
#[serde(default)]
cache_db_path: Option<toml::value::Value>,
#[serde(default)]
kanidm: Option<toml::value::Value>,
}
#[derive(Debug, Clone, Default)]
pub enum HsmType {
#[cfg_attr(not(feature = "tpm"), default)]
Soft,
#[cfg_attr(feature = "tpm", default)]
TpmIfPossible,
Tpm,
}
impl Display for HsmType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
HsmType::Soft => write!(f, "Soft"),
HsmType::TpmIfPossible => write!(f, "Tpm if possible"),
HsmType::Tpm => write!(f, "Tpm"),
}
}
}
#[derive(Debug)]
pub struct UnixdConfig {
pub cache_db_path: String,
pub sock_path: String,
pub task_sock_path: String,
pub cache_timeout: u64,
pub unix_sock_timeout: u64,
pub default_shell: String,
pub home_prefix: PathBuf,
pub home_mount_prefix: Option<PathBuf>,
pub home_attr: HomeAttr,
pub home_alias: Option<HomeAttr>,
pub use_etc_skel: bool,
pub uid_attr_map: UidAttr,
pub gid_attr_map: UidAttr,
pub selinux: bool,
pub hsm_type: HsmType,
pub hsm_pin_path: String,
pub tpm_tcti_name: String,
pub kanidm_config: Option<KanidmConfig>,
}
#[derive(Debug)]
pub struct KanidmConfig {
pub conn_timeout: u64,
pub request_timeout: u64,
pub pam_allowed_login_groups: Vec<String>,
pub map_group: Vec<GroupMap>,
}
impl Default for UnixdConfig {
fn default() -> Self {
UnixdConfig::new()
}
}
impl Display for UnixdConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "cache_db_path: {}", &self.cache_db_path)?;
writeln!(f, "sock_path: {}", self.sock_path)?;
writeln!(f, "task_sock_path: {}", self.task_sock_path)?;
writeln!(f, "unix_sock_timeout: {}", self.unix_sock_timeout)?;
writeln!(f, "cache_timeout: {}", self.cache_timeout)?;
writeln!(f, "default_shell: {}", self.default_shell)?;
writeln!(f, "home_prefix: {:?}", self.home_prefix)?;
match self.home_mount_prefix.as_deref() {
Some(val) => writeln!(f, "home_mount_prefix: {:?}", val)?,
None => writeln!(f, "home_mount_prefix: unset")?,
}
writeln!(f, "home_attr: {}", self.home_attr)?;
match self.home_alias {
Some(val) => writeln!(f, "home_alias: {}", val)?,
None => writeln!(f, "home_alias: unset")?,
}
writeln!(f, "uid_attr_map: {}", self.uid_attr_map)?;
writeln!(f, "gid_attr_map: {}", self.gid_attr_map)?;
writeln!(f, "hsm_type: {}", self.hsm_type)?;
writeln!(f, "tpm_tcti_name: {}", self.tpm_tcti_name)?;
writeln!(f, "selinux: {}", self.selinux)?;
if let Some(kconfig) = &self.kanidm_config {
writeln!(f, "kanidm: enabled")?;
writeln!(
f,
"kanidm pam_allowed_login_groups: {:#?}",
kconfig.pam_allowed_login_groups
)?;
writeln!(f, "kanidm conn_timeout: {}", kconfig.conn_timeout)?;
writeln!(f, "kanidm request_timeout: {}", kconfig.request_timeout)?;
} else {
writeln!(f, "kanidm: disabled")?;
};
Ok(())
}
}
impl UnixdConfig {
pub fn new() -> Self {
let cache_db_path = match env::var("KANIDM_CACHE_DB_PATH") {
Ok(val) => val,
Err(_) => DEFAULT_CACHE_DB_PATH.into(),
};
let hsm_pin_path = match env::var("KANIDM_HSM_PIN_PATH") {
Ok(val) => val,
Err(_) => DEFAULT_HSM_PIN_PATH.into(),
};
UnixdConfig {
cache_db_path,
sock_path: DEFAULT_SOCK_PATH.to_string(),
task_sock_path: DEFAULT_TASK_SOCK_PATH.to_string(),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: DEFAULT_CACHE_TIMEOUT,
default_shell: DEFAULT_SHELL.to_string(),
home_prefix: DEFAULT_HOME_PREFIX.into(),
home_mount_prefix: None,
home_attr: DEFAULT_HOME_ATTR,
home_alias: DEFAULT_HOME_ALIAS,
use_etc_skel: DEFAULT_USE_ETC_SKEL,
uid_attr_map: DEFAULT_UID_ATTR_MAP,
gid_attr_map: DEFAULT_GID_ATTR_MAP,
selinux: DEFAULT_SELINUX,
hsm_pin_path,
hsm_type: HsmType::default(),
tpm_tcti_name: DEFAULT_TPM_TCTI_NAME.to_string(),
kanidm_config: None,
}
}
pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
self,
config_path: P,
) -> Result<Self, UnixIntegrationError> {
debug!("Attempting to load configuration from {:#?}", &config_path);
let mut f = match File::open(&config_path) {
Ok(f) => {
debug!("Successfully opened configuration file {:#?}", &config_path);
f
}
Err(e) => {
match e.kind() {
ErrorKind::NotFound => {
debug!(
"Configuration file {:#?} not found, skipping.",
&config_path
);
}
ErrorKind::PermissionDenied => {
warn!(
"Permission denied loading configuration file {:#?}, skipping.",
&config_path
);
}
_ => {
debug!(
"Unable to open config file {:#?} [{:?}], skipping ...",
&config_path, e
);
}
};
return Ok(self);
}
};
let mut contents = String::new();
f.read_to_string(&mut contents).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
let config: ConfigUntagged = toml::from_str(contents.as_str()).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
match config {
ConfigUntagged::Legacy(config) => self.apply_from_config_legacy(config),
ConfigUntagged::Versioned(ConfigVersion::V2 { values }) => {
self.apply_from_config_v2(values)
}
}
}
fn apply_from_config_legacy(self, config: ConfigInt) -> Result<Self, UnixIntegrationError> {
if config.kanidm.is_some() || config.cache_db_path.is_some() {
error!("You are using version=\"2\" options in a legacy config. THESE WILL NOT WORK.");
return Err(UnixIntegrationError);
}
let map_group = config
.allow_local_account_override
.iter()
.map(|name| GroupMap {
local: name.clone(),
with: name.clone(),
})
.collect();
let kanidm_config = Some(KanidmConfig {
conn_timeout: config.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
request_timeout: config.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
pam_allowed_login_groups: config.pam_allowed_login_groups.unwrap_or_default(),
map_group,
});
// Now map the values into our config.
Ok(UnixdConfig {
cache_db_path: config.db_path.unwrap_or(self.cache_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),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
default_shell: config.default_shell.unwrap_or(self.default_shell),
home_prefix: config
.home_prefix
.map(|p| p.into())
.unwrap_or(self.home_prefix.clone()),
home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
home_attr: config
.home_attr
.and_then(|v| match v.as_str() {
"uuid" => Some(HomeAttr::Uuid),
"spn" => Some(HomeAttr::Spn),
"name" => Some(HomeAttr::Name),
_ => {
warn!("Invalid home_attr configured, using default ...");
None
}
})
.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),
use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
uid_attr_map: config
.uid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid uid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.uid_attr_map),
gid_attr_map: config
.gid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid gid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.gid_attr_map),
selinux: match config.selinux.unwrap_or(self.selinux) {
#[cfg(all(target_family = "unix", feature = "selinux"))]
true => selinux_util::supported(),
_ => false,
},
hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
hsm_type: config
.hsm_type
.and_then(|v| match v.as_str() {
"soft" => Some(HsmType::Soft),
"tpm_if_possible" => Some(HsmType::TpmIfPossible),
"tpm" => Some(HsmType::Tpm),
_ => {
warn!("Invalid hsm_type configured, using default ...");
None
}
})
.unwrap_or(self.hsm_type),
tpm_tcti_name: config
.tpm_tcti_name
.unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
kanidm_config,
})
}
fn apply_from_config_v2(self, config: ConfigV2) -> Result<Self, UnixIntegrationError> {
let kanidm_config = if let Some(kconfig) = config.kanidm {
Some(KanidmConfig {
conn_timeout: kconfig.conn_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT),
request_timeout: kconfig.request_timeout.unwrap_or(DEFAULT_CONN_TIMEOUT * 2),
pam_allowed_login_groups: kconfig.pam_allowed_login_groups.unwrap_or_default(),
map_group: kconfig.map_group,
})
} else {
None
};
// Now map the values into our config.
Ok(UnixdConfig {
cache_db_path: config.cache_db_path.unwrap_or(self.cache_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),
unix_sock_timeout: DEFAULT_CONN_TIMEOUT * 2,
cache_timeout: config.cache_timeout.unwrap_or(self.cache_timeout),
default_shell: config.default_shell.unwrap_or(self.default_shell),
home_prefix: config
.home_prefix
.map(|p| p.into())
.unwrap_or(self.home_prefix.clone()),
home_mount_prefix: config.home_mount_prefix.map(|p| p.into()),
home_attr: config
.home_attr
.and_then(|v| match v.as_str() {
"uuid" => Some(HomeAttr::Uuid),
"spn" => Some(HomeAttr::Spn),
"name" => Some(HomeAttr::Name),
_ => {
warn!("Invalid home_attr configured, using default ...");
None
}
})
.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),
use_etc_skel: config.use_etc_skel.unwrap_or(self.use_etc_skel),
uid_attr_map: config
.uid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid uid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.uid_attr_map),
gid_attr_map: config
.gid_attr_map
.and_then(|v| match v.as_str() {
"spn" => Some(UidAttr::Spn),
"name" => Some(UidAttr::Name),
_ => {
warn!("Invalid gid_attr_map configured, using default ...");
None
}
})
.unwrap_or(self.gid_attr_map),
selinux: match config.selinux.unwrap_or(self.selinux) {
#[cfg(all(target_family = "unix", feature = "selinux"))]
true => selinux_util::supported(),
_ => false,
},
hsm_pin_path: config.hsm_pin_path.unwrap_or(self.hsm_pin_path),
hsm_type: config
.hsm_type
.and_then(|v| match v.as_str() {
"soft" => Some(HsmType::Soft),
"tpm_if_possible" => Some(HsmType::TpmIfPossible),
"tpm" => Some(HsmType::Tpm),
_ => {
warn!("Invalid hsm_type configured, using default ...");
None
}
})
.unwrap_or(self.hsm_type),
tpm_tcti_name: config
.tpm_tcti_name
.unwrap_or(DEFAULT_TPM_TCTI_NAME.to_string()),
kanidm_config,
})
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
#[test]
fn test_load_example_configs() {
// Test the various included configs
let examples_dir = env!("CARGO_MANIFEST_DIR").to_string() + "/../../examples/";
for file in PathBuf::from(&examples_dir)
.canonicalize()
.expect(&format!("Can't find examples dir at {}", examples_dir))
.read_dir()
.expect("Can't read examples dir!")
{
let file = file.unwrap();
let filename = file.file_name().into_string().unwrap();
if filename.starts_with("unixd") {
print!("Checking that {} parses as a valid config...", filename);
UnixdConfig::new()
.read_options_from_optional_config(file.path())
.inspect_err(|e| {
println!("Failed to parse: {:?}", e);
})
.expect("Failed to parse!");
println!("OK");
}
}
}
}

View file

@ -12,13 +12,13 @@ use kanidm_unix_common::constants::{
DEFAULT_GID_ATTR_MAP, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX,
DEFAULT_SHELL, DEFAULT_UID_ATTR_MAP,
};
use kanidm_unix_common::unix_config::{GroupMap, KanidmConfig};
use kanidm_unix_common::unix_passwd::{CryptPw, EtcGroup, EtcShadow, EtcUser};
use kanidm_unix_resolver::db::{Cache, Db};
use kanidm_unix_resolver::idprovider::interface::Id;
use kanidm_unix_resolver::idprovider::kanidm::KanidmProvider;
use kanidm_unix_resolver::idprovider::system::SystemProvider;
use kanidm_unix_resolver::resolver::Resolver;
use kanidm_unix_resolver::unix_config::{GroupMap, KanidmConfig};
use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole};
use kanidmd_core::create_server_core;
use kanidmd_testkit::{is_free_port, PORT_ALLOC};