Compare commits

...

3 commits

Author SHA1 Message Date
ChanceHarrison b5c9435de9
Merge cc9433fbd4 into 9611a7f976 2025-02-21 11:58:03 +01:00
Sebastiano Tocci 9611a7f976
Fixes : add configurable maximum queryable attributes for LDAP () 2025-02-21 12:14:47 +10:00
Chance Harrison cc9433fbd4
docs(faq): Discuss options for TLS between LB and kanidm 2024-04-28 22:58:00 -07:00
15 changed files with 229 additions and 10 deletions
book/src
libs/client/src
proto/src
server
tools/cli/src
cli/domain
opt

View file

@ -52,6 +52,19 @@ configured.
Similarly, WebAuthn and its various other names like Passkeys, FIDO2 or "scan the QR code to log in"
will [only work over TLS](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).
There are a variety of ways that you can configure TLS between your load balancer and Kanidm.
Ultimately, any option that maintains the confidentiality and integrity of the communication will
suffice. Some options include, but are not limited to:
- Generating a self-signed certificate
- Utilize certificate pinning to ensure that the load balancer only trusts connections made with
that particular certificate
- Not terminating TLS / TLS passthrough / TCP proxy
- Running your own certificate authority (CA)
The "best" option for you will depend on a number of factors, including your threat model and the
specifc load balancer you are using.
## OAuth2
[RFC6819 - OAuth2 Threat Model and Security Considerations](https://www.rfc-editor.org/rfc/rfc6819)

View file

@ -30,8 +30,8 @@ use compact_jwt::Jwk;
use kanidm_proto::constants::uri::V1_AUTH_VALID;
use kanidm_proto::constants::{
ATTR_DOMAIN_DISPLAY_NAME, ATTR_DOMAIN_LDAP_BASEDN, ATTR_DOMAIN_SSID, ATTR_ENTRY_MANAGED_BY,
ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_NAME, CLIENT_TOKEN_CACHE, KOPID,
KSESSIONID, KVERSION,
ATTR_KEY_ACTION_REVOKE, ATTR_LDAP_ALLOW_UNIX_PW_BIND, ATTR_LDAP_MAX_QUERYABLE_ATTRS, ATTR_NAME,
CLIENT_TOKEN_CACHE, KOPID, KSESSIONID, KVERSION,
};
use kanidm_proto::internal::*;
use kanidm_proto::v1::*;
@ -2082,6 +2082,18 @@ impl KanidmClient {
.await
}
/// Sets the maximum number of LDAP attributes that can be queryed in a single operation
pub async fn idm_domain_set_ldap_max_queryable_attrs(
&self,
max_queryable_attrs: usize,
) -> Result<(), ClientError> {
self.perform_put_request(
&format!("/v1/domain/_attr/{}", ATTR_LDAP_MAX_QUERYABLE_ATTRS),
vec![max_queryable_attrs.to_string()],
)
.await
}
pub async fn idm_set_ldap_allow_unix_password_bind(
&self,
enable: bool,

View file

@ -94,6 +94,7 @@ pub enum Attribute {
LdapEmailAddress,
/// An LDAP Compatible sshkeys virtual attribute
LdapKeys,
LdapMaxQueryableAttrs,
LegalName,
LimitSearchMaxResults,
LimitSearchMaxFilterTest,
@ -322,6 +323,7 @@ impl Attribute {
Attribute::LdapAllowUnixPwBind => ATTR_LDAP_ALLOW_UNIX_PW_BIND,
Attribute::LdapEmailAddress => ATTR_LDAP_EMAIL_ADDRESS,
Attribute::LdapKeys => ATTR_LDAP_KEYS,
Attribute::LdapMaxQueryableAttrs => ATTR_LDAP_MAX_QUERYABLE_ATTRS,
Attribute::LdapSshPublicKey => ATTR_LDAP_SSHPUBLICKEY,
Attribute::LegalName => ATTR_LEGALNAME,
Attribute::LimitSearchMaxResults => ATTR_LIMIT_SEARCH_MAX_RESULTS,
@ -505,6 +507,7 @@ impl Attribute {
ATTR_LDAP_ALLOW_UNIX_PW_BIND => Attribute::LdapAllowUnixPwBind,
ATTR_LDAP_EMAIL_ADDRESS => Attribute::LdapEmailAddress,
ATTR_LDAP_KEYS => Attribute::LdapKeys,
ATTR_LDAP_MAX_QUERYABLE_ATTRS => Attribute::LdapMaxQueryableAttrs,
ATTR_SSH_PUBLICKEY => Attribute::SshPublicKey,
ATTR_LEGALNAME => Attribute::LegalName,
ATTR_LINKEDGROUP => Attribute::LinkedGroup,

View file

@ -39,6 +39,8 @@ pub const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:8443";
pub const DEFAULT_SERVER_LOCALHOST: &str = "localhost:8443";
/// The default LDAP bind address for the Kanidm client
pub const DEFAULT_LDAP_LOCALHOST: &str = "localhost:636";
/// The default amount of attributes that can be queried in LDAP
pub const DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: usize = 16;
/// Default replication configuration
pub const DEFAULT_REPLICATION_ADDRESS: &str = "127.0.0.1:8444";
pub const DEFAULT_REPLICATION_ORIGIN: &str = "repl://localhost:8444";
@ -102,6 +104,7 @@ pub const ATTR_DYNGROUP_FILTER: &str = "dyngroup_filter";
pub const ATTR_DYNGROUP: &str = "dyngroup";
pub const ATTR_DYNMEMBER: &str = "dynmember";
pub const ATTR_LDAP_EMAIL_ADDRESS: &str = "emailaddress";
pub const ATTR_LDAP_MAX_QUERYABLE_ATTRS: &str = "ldap_max_queryable_attrs";
pub const ATTR_EMAIL_ALTERNATIVE: &str = "emailalternative";
pub const ATTR_EMAIL_PRIMARY: &str = "emailprimary";
pub const ATTR_EMAIL: &str = "email";

View file

@ -116,7 +116,6 @@ pub struct ServerConfig {
///
/// If unset, the LDAP server will be disabled.
pub ldapbindaddress: Option<String>,
/// The role of this server, one of write_replica, write_replica_no_ui, read_only_replica, defaults to [ServerRole::WriteReplica]
#[serde(default)]
pub role: ServerRole,

View file

@ -1045,6 +1045,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::DomainUuid,
// Grants read access to the key object.
@ -1058,6 +1059,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
@ -1065,6 +1067,7 @@ lazy_static! {
modify_present_attrs: vec![
Attribute::DomainDisplayName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
@ -1100,6 +1103,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::DomainUuid,
Attribute::KeyInternalData,
@ -1111,6 +1115,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
Attribute::KeyActionRotate,
@ -1119,6 +1124,7 @@ lazy_static! {
modify_present_attrs: vec![
Attribute::DomainDisplayName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
@ -1156,6 +1162,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::DomainUuid,
Attribute::KeyInternalData,
@ -1167,6 +1174,7 @@ lazy_static! {
Attribute::DomainDisplayName,
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainAllowEasterEggs,
Attribute::LdapAllowUnixPwBind,
Attribute::KeyActionRevoke,
@ -1176,6 +1184,7 @@ lazy_static! {
modify_present_attrs: vec![
Attribute::DomainDisplayName,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::DomainSsid,
Attribute::DomainAllowEasterEggs,
Attribute::LdapAllowUnixPwBind,

View file

@ -167,6 +167,17 @@ pub static ref SCHEMA_ATTR_DOMAIN_LDAP_BASEDN: SchemaAttribute = SchemaAttribute
..Default::default()
};
pub static ref SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES,
name: Attribute::LdapMaxQueryableAttrs,
description: "The maximum number of LDAP attributes that can be queried in one operation".to_string(),
multivalue: false,
sync_allowed: true,
syntax: SyntaxType::Uint32,
..Default::default()
};
pub static ref SCHEMA_ATTR_DOMAIN_DISPLAY_NAME: SchemaAttribute = SchemaAttribute {
uuid: UUID_SCHEMA_ATTR_DOMAIN_DISPLAY_NAME,
name: Attribute::DomainDisplayName,
@ -1227,6 +1238,7 @@ pub static ref SCHEMA_CLASS_DOMAIN_INFO_DL10: SchemaClass = SchemaClass {
systemmay: vec![
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::Image,
Attribute::PatchLevel,

View file

@ -131,7 +131,8 @@ pub const UUID_SCHEMA_ATTR_PRIMARY_CREDENTIAL: Uuid = uuid!("00000000-0000-0000-
pub const UUID_SCHEMA_CLASS_PERSON: Uuid = uuid!("00000000-0000-0000-0000-ffff00000044");
pub const UUID_SCHEMA_CLASS_GROUP: Uuid = uuid!("00000000-0000-0000-0000-ffff00000045");
pub const UUID_SCHEMA_CLASS_ACCOUNT: Uuid = uuid!("00000000-0000-0000-0000-ffff00000046");
// GAP - 47
pub const UUID_SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES: Uuid =
uuid!("00000000-0000-0000-0000-ffff00000187");
pub const UUID_SCHEMA_ATTR_ATTRIBUTENAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000048");
pub const UUID_SCHEMA_ATTR_CLASSNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000049");
pub const UUID_SCHEMA_ATTR_LEGALNAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000050");

View file

@ -60,6 +60,7 @@ pub struct LdapServer {
basedn: String,
dnre: Regex,
binddnre: Regex,
max_queryable_attrs: usize,
}
#[derive(Debug)]
@ -79,6 +80,12 @@ impl LdapServer {
.qs_read
.internal_search_uuid(UUID_DOMAIN_INFO)?;
// Get the maximum number of queryable attributes from the domain entry
let max_queryable_attrs = domain_entry
.get_ava_single_uint32(Attribute::LdapMaxQueryableAttrs)
.map(|u| u as usize)
.unwrap_or(DEFAULT_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES);
let basedn = domain_entry
.get_ava_single_iutf8(Attribute::DomainLdapBasedn)
.map(|s| s.to_string())
@ -154,6 +161,7 @@ impl LdapServer {
basedn,
dnre,
binddnre,
max_queryable_attrs,
})
}
@ -239,11 +247,11 @@ impl LdapServer {
let mut all_attrs = false;
let mut all_op_attrs = false;
// TODO #3406: limit the number of attributes here!
let attrs_len = sr.attrs.len();
if sr.attrs.is_empty() {
// If [], then "all" attrs
all_attrs = true;
} else {
} else if attrs_len < self.max_queryable_attrs {
sr.attrs.iter().for_each(|a| {
if a == "*" {
all_attrs = true;
@ -267,6 +275,12 @@ impl LdapServer {
}
}
})
} else {
admin_error!(
"Too many LDAP attributes requested. Maximum allowed is {}, while your search query had {}",
self.max_queryable_attrs, attrs_len
);
return Err(OperationError::ResourceLimit);
}
// We need to retain this to know what the client requested.
@ -2631,4 +2645,106 @@ mod tests {
&OperationError::InvalidAttributeName("invalid".to_string()),
);
}
#[idm_test]
async fn test_ldap_maximum_queryable_attributes(
idms: &IdmServer,
_idms_delayed: &IdmServerDelayed,
) {
// Set the max queryable attrs to 2
let mut server_txn = idms.proxy_write(duration_from_epoch_now()).await.unwrap();
let set_ldap_maximum_queryable_attrs = ModifyEvent::new_internal_invalid(
filter!(f_eq(Attribute::Uuid, PartialValue::Uuid(UUID_DOMAIN_INFO))),
ModifyList::new_purge_and_set(Attribute::LdapMaxQueryableAttrs, Value::Uint32(2)),
);
assert!(server_txn
.qs_write
.modify(&set_ldap_maximum_queryable_attrs)
.and_then(|_| server_txn.commit())
.is_ok());
let ldaps = LdapServer::new(idms).await.expect("failed to start ldap");
let usr_uuid = Uuid::new_v4();
let grp_uuid = Uuid::new_v4();
let app_uuid = Uuid::new_v4();
let app_name = "testapp1";
// Setup person, group and application
{
let e1 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Account.to_value()),
(Attribute::Class, EntryClass::Person.to_value()),
(Attribute::Name, Value::new_iname("testperson1")),
(Attribute::Uuid, Value::Uuid(usr_uuid)),
(Attribute::Description, Value::new_utf8s("testperson1")),
(Attribute::DisplayName, Value::new_utf8s("testperson1"))
);
let e2 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::Group.to_value()),
(Attribute::Name, Value::new_iname("testgroup1")),
(Attribute::Uuid, Value::Uuid(grp_uuid))
);
let e3 = entry_init!(
(Attribute::Class, EntryClass::Object.to_value()),
(Attribute::Class, EntryClass::ServiceAccount.to_value()),
(Attribute::Class, EntryClass::Application.to_value()),
(Attribute::Name, Value::new_iname(app_name)),
(Attribute::Uuid, Value::Uuid(app_uuid)),
(Attribute::LinkedGroup, Value::Refer(grp_uuid))
);
let ct = duration_from_epoch_now();
let mut server_txn = idms.proxy_write(ct).await.unwrap();
assert!(server_txn
.qs_write
.internal_create(vec![e1, e2, e3])
.and_then(|_| server_txn.commit())
.is_ok());
}
// Setup the anonymous login
let anon_t = ldaps.do_bind(idms, "", "").await.unwrap().unwrap();
assert_eq!(
anon_t.effective_session,
LdapSession::UnixBind(UUID_ANONYMOUS)
);
let invalid_search = SearchRequest {
msgid: 1,
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec![
"objectClass".to_string(),
"cn".to_string(),
"givenName".to_string(),
],
};
let valid_search = SearchRequest {
msgid: 1,
base: "dc=example,dc=com".to_string(),
scope: LdapSearchScope::Subtree,
filter: LdapFilter::Present(Attribute::ObjectClass.to_string()),
attrs: vec!["objectClass: person".to_string()],
};
let invalid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
.do_search(idms, &invalid_search, &anon_t, Source::Internal)
.await;
let valid_res: Result<Vec<LdapMsg>, OperationError> = ldaps
.do_search(idms, &valid_search, &anon_t, Source::Internal)
.await;
assert_eq!(invalid_res, Err(OperationError::ResourceLimit));
assert!(valid_res.is_ok());
}
}

View file

@ -25,6 +25,7 @@ lazy_static! {
// modification of some domain info types for local configuratiomn.
Attribute::DomainSsid,
Attribute::DomainLdapBasedn,
Attribute::LdapMaxQueryableAttrs,
Attribute::LdapAllowUnixPwBind,
Attribute::FernetPrivateKeyStr,
Attribute::Es256PrivateKeyDer,

View file

@ -430,6 +430,7 @@ impl QueryServerWriteTransaction<'_> {
let idm_schema_changes = [
SCHEMA_ATTR_DENIED_NAME_DL10.clone().into(),
SCHEMA_CLASS_DOMAIN_INFO_DL10.clone().into(),
SCHEMA_ATTR_LDAP_MAXIMUM_QUERYABLE_ATTRIBUTES.clone().into(),
];
idm_schema_changes

View file

@ -26,6 +26,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
.expect("Failed to set idm_domain_set_ldap_basedn");
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
rsclient
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
.await
.expect("Failed to login as admin");
rsclient
.idm_domain_set_ldap_max_queryable_attrs(30)
.await
.expect("Failed to set idm_domain_set_ldap_max_queryable_attrs");
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_display_name(rsclient: KanidmClient) {
rsclient

View file

@ -231,6 +231,19 @@ async fn test_idm_domain_set_ldap_basedn(rsclient: KanidmClient) {
.is_err());
}
#[kanidmd_testkit::test]
async fn test_idm_domain_set_ldap_max_queryable_attrs(rsclient: KanidmClient) {
login_put_admin_idm_admins(&rsclient).await;
assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(20)
.await
.is_ok());
assert!(rsclient
.idm_domain_set_ldap_max_queryable_attrs(10)
.await
.is_ok()); // Ideally this should be "is_err"
}
#[kanidmd_testkit::test]
/// Checks that a built-in group idm_all_persons has the "builtin" class as expected.
async fn test_all_persons_has_builtin_class(rsclient: KanidmClient) {

View file

@ -14,7 +14,8 @@ impl DomainOpt {
| DomainOpt::SetLdapAllowUnixPasswordBind { copt, .. }
| DomainOpt::SetAllowEasterEggs { copt, .. }
| DomainOpt::RevokeKey { copt, .. }
| DomainOpt::Show(copt) => copt.debug,
| DomainOpt::Show(copt)
| DomainOpt::SetLdapMaxQueryableAttrs { copt, .. } => copt.debug,
}
}
@ -34,6 +35,23 @@ impl DomainOpt {
Err(e) => handle_client_error(e, opt.copt.output_mode),
}
}
DomainOpt::SetLdapMaxQueryableAttrs {
copt,
new_max_queryable_attrs,
} => {
eprintln!(
"Attempting to set the maximum number of queryable LDAP attributes to: {:?}",
new_max_queryable_attrs
);
let client = copt.to_client(OpType::Write).await;
match client
.idm_domain_set_ldap_max_queryable_attrs(*new_max_queryable_attrs)
.await
{
Ok(_) => println!("Success"),
Err(e) => handle_client_error(e, copt.output_mode),
}
}
DomainOpt::SetLdapBasedn { copt, new_basedn } => {
eprintln!(
"Attempting to set the domain's ldap basedn to: {:?}",

View file

@ -205,7 +205,6 @@ pub enum GroupAccountPolicyOpt {
copt: CommonOpt,
},
/// Set the maximum time for privilege session expiry in seconds.
#[clap(name = "privilege-expiry")]
PrivilegedSessionExpiry {
@ -215,7 +214,6 @@ pub enum GroupAccountPolicyOpt {
copt: CommonOpt,
},
/// The WebAuthn attestation CA list that should be enforced
/// on members of this group. Prevents use of passkeys that are
/// not in this list. To create this list, use `fido-mds-tool`
@ -301,7 +299,6 @@ pub enum GroupAccountPolicyOpt {
#[clap(flatten)]
copt: CommonOpt,
},
}
#[derive(Debug, Subcommand)]
@ -1320,6 +1317,14 @@ pub enum DomainOpt {
#[clap[name = "set-displayname"]]
/// Set the domain display name
SetDisplayname(OptSetDomainDisplayname),
/// Sets the maximum number of LDAP attributes that can be queried in one operation.
#[clap[name = "set-ldap-queryable-attrs"]]
SetLdapMaxQueryableAttrs {
#[clap(flatten)]
copt: CommonOpt,
#[clap(name = "maximum-queryable-attrs")]
new_max_queryable_attrs: usize,
},
#[clap[name = "set-ldap-basedn"]]
/// Change the basedn of this server. Takes effect after a server restart.
/// Examples are `o=organisation` or `dc=domain,dc=name`. Must be a valid ldap