mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Fix issues with radius (#1084)
This commit is contained in:
parent
2e9a94e703
commit
aa9af0705c
|
@ -125,8 +125,9 @@ verify_hostnames = true # verify the hostname of the Kanidm server
|
|||
|
||||
verify_ca = false # Strict CA verification
|
||||
ca = /data/ca.pem # Path to the kanidm ca
|
||||
username = # Username of the RADIUS service account
|
||||
password = # Generated secret for the service account
|
||||
|
||||
auth_token = "ABC..." # Auth token for the service account
|
||||
# See: kanidm service-account api-token generate
|
||||
|
||||
# Default vlans for groups that don't specify one.
|
||||
radius_default_vlan = 1
|
||||
|
@ -134,12 +135,12 @@ radius_default_vlan = 1
|
|||
# A list of Kanidm groups which must be a member
|
||||
# before they can authenticate via RADIUS.
|
||||
radius_required_groups = [
|
||||
"radius_access_allowed",
|
||||
"radius_access_allowed@idm.example.com",
|
||||
]
|
||||
|
||||
# A mapping between Kanidm groups and VLANS
|
||||
radius_groups = [
|
||||
{ name = "radius_access_allowed", vlan = 10 },
|
||||
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 },
|
||||
]
|
||||
|
||||
# A mapping of clients and their authentication tokens
|
||||
|
@ -164,9 +165,8 @@ radius_clients = [
|
|||
```toml
|
||||
url = "https://example.com"
|
||||
|
||||
username = "radius_service_account"
|
||||
# The generated password from above
|
||||
password = "cr4bzr0ol"
|
||||
# The auth token for the service account
|
||||
auth_token = "ABC..."
|
||||
|
||||
# default vlan for groups that don't specify one.
|
||||
radius_default_vlan = 99
|
||||
|
@ -174,11 +174,11 @@ radius_default_vlan = 99
|
|||
# if the user is in one of these Kanidm groups,
|
||||
# then they're allowed to authenticate
|
||||
radius_required_groups = [
|
||||
"radius_access_allowed",
|
||||
"radius_access_allowed@idm.example.com",
|
||||
]
|
||||
|
||||
radius_groups = [
|
||||
{ name = "radius_access_allowed", vlan = 10 }
|
||||
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 }
|
||||
]
|
||||
|
||||
radius_clients = [
|
||||
|
|
|
@ -50,6 +50,7 @@ RUN rm -rf /pkg/*
|
|||
|
||||
USER radiusd
|
||||
ENV LD_PRELOAD=/usr/lib64/libpython3.so
|
||||
ENV KANIDM_CONFIG_FILE="/data/kanidm"
|
||||
|
||||
COPY kanidm_rlm_python/radius_entrypoint.py /radius_entrypoint.py
|
||||
CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ]
|
||||
|
|
|
@ -155,7 +155,7 @@ impl QueryServerReadV1 {
|
|||
#[instrument(
|
||||
level = "info",
|
||||
name = "online_backup",
|
||||
skip(self, msg, outpath, versions)
|
||||
skip_all,
|
||||
fields(uuid = ?msg.eventid)
|
||||
)]
|
||||
pub async fn handle_online_backup(
|
||||
|
|
|
@ -162,45 +162,45 @@ macro_rules! get_idl {
|
|||
$itype:expr,
|
||||
$idx_key:expr
|
||||
) => {{
|
||||
// SEE ALSO #259: Find a way to implement borrow for this properly.
|
||||
// I don't think this is possible. When we make this dyn, the arc
|
||||
// needs the dyn trait to be sized so that it *could* claim a clone
|
||||
// for hit tracking reasons. That also means that we need From and
|
||||
// some other traits that just seem incompatible. And in the end,
|
||||
// we clone a few times in arc, and if we miss we need to insert anyway
|
||||
//
|
||||
// So the best path could be to replace IdlCacheKey with a compressed
|
||||
// or smaller type. Perhaps even a small cache of the IdlCacheKeys that
|
||||
// are allocated to reduce some allocs? Probably over thinking it at
|
||||
// this point.
|
||||
//
|
||||
// First attempt to get from this cache.
|
||||
let cache_key = IdlCacheKeyRef {
|
||||
a: $attr,
|
||||
i: $itype,
|
||||
k: $idx_key,
|
||||
};
|
||||
let cache_r = $self.idl_cache.get(&cache_key as &dyn IdlCacheKeyToRef);
|
||||
// If hit, continue.
|
||||
if let Some(ref data) = cache_r {
|
||||
trace!(
|
||||
cached_index = ?$itype,
|
||||
attr = ?$attr,
|
||||
idl = %data,
|
||||
);
|
||||
return Ok(Some(data.as_ref().clone()));
|
||||
}
|
||||
// If miss, get from db *and* insert to the cache.
|
||||
let db_r = $self.db.get_idl($attr, $itype, $idx_key)?;
|
||||
if let Some(ref idl) = db_r {
|
||||
let ncache_key = IdlCacheKey {
|
||||
a: $attr.into(),
|
||||
i: $itype.clone(),
|
||||
k: $idx_key.into(),
|
||||
};
|
||||
$self.idl_cache.insert(ncache_key, Box::new(idl.clone()))
|
||||
}
|
||||
Ok(db_r)
|
||||
// SEE ALSO #259: Find a way to implement borrow for this properly.
|
||||
// I don't think this is possible. When we make this dyn, the arc
|
||||
// needs the dyn trait to be sized so that it *could* claim a clone
|
||||
// for hit tracking reasons. That also means that we need From and
|
||||
// some other traits that just seem incompatible. And in the end,
|
||||
// we clone a few times in arc, and if we miss we need to insert anyway
|
||||
//
|
||||
// So the best path could be to replace IdlCacheKey with a compressed
|
||||
// or smaller type. Perhaps even a small cache of the IdlCacheKeys that
|
||||
// are allocated to reduce some allocs? Probably over thinking it at
|
||||
// this point.
|
||||
//
|
||||
// First attempt to get from this cache.
|
||||
let cache_key = IdlCacheKeyRef {
|
||||
a: $attr,
|
||||
i: $itype,
|
||||
k: $idx_key,
|
||||
};
|
||||
let cache_r = $self.idl_cache.get(&cache_key as &dyn IdlCacheKeyToRef);
|
||||
// If hit, continue.
|
||||
if let Some(ref data) = cache_r {
|
||||
trace!(
|
||||
cached_index = ?$itype,
|
||||
attr = ?$attr,
|
||||
idl = %data,
|
||||
);
|
||||
return Ok(Some(data.as_ref().clone()));
|
||||
}
|
||||
// If miss, get from db *and* insert to the cache.
|
||||
let db_r = $self.db.get_idl($attr, $itype, $idx_key)?;
|
||||
if let Some(ref idl) = db_r {
|
||||
let ncache_key = IdlCacheKey {
|
||||
a: $attr.into(),
|
||||
i: $itype.clone(),
|
||||
k: $idx_key.into(),
|
||||
};
|
||||
$self.idl_cache.insert(ncache_key, Box::new(idl.clone()))
|
||||
}
|
||||
Ok(db_r)
|
||||
}};
|
||||
}
|
||||
|
||||
|
@ -366,6 +366,7 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteReadTransaction<'a> {
|
|||
exists_idx!(self, attr, itype)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn get_idl(
|
||||
&mut self,
|
||||
attr: &str,
|
||||
|
@ -447,6 +448,7 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteWriteTransaction<'a> {
|
|||
exists_idx!(self, attr, itype)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn get_idl(
|
||||
&mut self,
|
||||
attr: &str,
|
||||
|
|
|
@ -210,6 +210,7 @@ pub trait IdlSqliteTransaction {
|
|||
self.exists_table(&tname)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
fn get_idl(
|
||||
&self,
|
||||
attr: &str,
|
||||
|
@ -247,7 +248,6 @@ pub trait IdlSqliteTransaction {
|
|||
// have a corrupted index .....
|
||||
None => IDLBitRange::new(),
|
||||
};
|
||||
|
||||
trace!(
|
||||
miss_index = ?itype,
|
||||
attr = ?attr,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" kanidm RADIUS module """
|
||||
|
||||
""" kanidm RADIUS module """
|
||||
import asyncio
|
||||
from functools import reduce
|
||||
import json
|
||||
|
@ -9,68 +9,56 @@ from pathlib import Path
|
|||
import sys
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
from kanidm import KanidmClient
|
||||
from kanidm.types import AuthStepPasswordResponse, RadiusTokenGroup, RadiusTokenResponse
|
||||
from kanidm.utils import load_config
|
||||
from kanidm.exceptions import NoMatchingEntries
|
||||
from kanidm.types import AuthStepPasswordResponse, RadiusTokenResponse
|
||||
|
||||
from .. import KanidmClient
|
||||
from . import radiusd
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
stream=sys.stderr,
|
||||
)
|
||||
from .utils import check_vlan
|
||||
|
||||
# the list of places to try
|
||||
config_paths = [
|
||||
CONFIG_PATHS = [
|
||||
os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness
|
||||
"~/.config/kanidm", # for a user
|
||||
"/etc/kanidm/kanidm", # system-wide
|
||||
"../examples/kanidm", # test mode
|
||||
]
|
||||
|
||||
CONFIG_PATH = None
|
||||
for config_file_path in config_paths:
|
||||
CONFIG_PATH = Path(config_file_path).expanduser().resolve()
|
||||
if CONFIG_PATH.exists():
|
||||
break
|
||||
|
||||
if (CONFIG_PATH is None) or (not CONFIG_PATH.exists()):
|
||||
logging.error(
|
||||
"Failed to find configuration file, checked (%s), quitting!", config_paths
|
||||
def instantiate(_: Any) -> Any:
|
||||
"""start up radiusd"""
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
stream=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
config = load_config(str(CONFIG_PATH))
|
||||
|
||||
KANIDM_CLIENT = KanidmClient(config_file=CONFIG_PATH)
|
||||
if KANIDM_CLIENT.config.auth_token is None:
|
||||
logging.error("You need to specify auth_token in the configuration file!")
|
||||
sys.exit(1)
|
||||
logging.info("Starting up!")
|
||||
|
||||
|
||||
def authenticate(
|
||||
acct: str,
|
||||
password: str,
|
||||
kanidm_client: KanidmClient = KANIDM_CLIENT,
|
||||
) -> Union[int, AuthStepPasswordResponse]:
|
||||
"""authenticate the RADIUS service account to Kanidm"""
|
||||
logging.error("authenticate - %s:%s", acct, password)
|
||||
config_path = None
|
||||
for config_file_path in CONFIG_PATHS:
|
||||
config_path = Path(config_file_path).expanduser().resolve()
|
||||
if config_path.exists():
|
||||
break
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(kanidm_client.check_token_valid())
|
||||
except Exception as error_message: # pylint: disable=broad-except
|
||||
logging.error("Failed to run kanidm.check_token_valid: %s", error_message)
|
||||
return radiusd.RLM_MODULE_FAIL
|
||||
if (config_path is None) or (not config_path.exists()):
|
||||
logging.error(
|
||||
"Failed to find configuration file, checked (%s), quitting!", CONFIG_PATHS
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
kanidm_client = KanidmClient(config_file=config_path)
|
||||
if kanidm_client.config.auth_token is None:
|
||||
logging.error("You need to specify auth_token in the configuration file!")
|
||||
sys.exit(1)
|
||||
os.environ["KANIDM_CONFIG_FILE"] = config_path.as_posix()
|
||||
logging.info("Config file: %s", config_path.as_posix())
|
||||
return radiusd.RLM_MODULE_OK
|
||||
|
||||
async def _get_radius_token(
|
||||
username: Optional[str] = None,
|
||||
kanidm_client: KanidmClient = KANIDM_CLIENT,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""pulls the radius token for a client username"""
|
||||
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
|
||||
if username is None:
|
||||
raise ValueError("Didn't get a username for _get_radius_token")
|
||||
# authenticate as the radius service account
|
||||
|
@ -86,45 +74,13 @@ async def _get_radius_token(
|
|||
logging.debug(response.data)
|
||||
return response.data
|
||||
|
||||
|
||||
def check_vlan(
|
||||
acc: int,
|
||||
group: RadiusTokenGroup,
|
||||
kanidm_client: Optional[KanidmClient] = None,
|
||||
) -> int:
|
||||
"""checks if a vlan is in the config,
|
||||
|
||||
acc is the default vlan
|
||||
"""
|
||||
logging.debug("acc=%s", acc)
|
||||
if kanidm_client is None:
|
||||
kanidm_client = KANIDM_CLIENT
|
||||
# raise ValueError("Need to pass this a kanidm_client")
|
||||
|
||||
for radius_group in kanidm_client.config.radius_groups:
|
||||
group_name = group.spn.split("@")[0]
|
||||
logging.debug(
|
||||
"Checking '%s' radius_group against group %s", radius_group, group_name
|
||||
)
|
||||
if radius_group.name == group_name:
|
||||
return radius_group.vlan
|
||||
logging.debug("returning default vlan: %s", acc)
|
||||
return acc
|
||||
|
||||
|
||||
def instantiate(_: Any) -> Any:
|
||||
"""start up radiusd"""
|
||||
logging.info("Starting up!")
|
||||
return radiusd.RLM_MODULE_OK
|
||||
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def authorize(
|
||||
args: Any = Dict[Any, Any],
|
||||
kanidm_client: KanidmClient = KANIDM_CLIENT,
|
||||
) -> Any:
|
||||
"""does the kanidm authorize step"""
|
||||
logging.info("kanidm python module called")
|
||||
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
|
||||
# args comes in like this
|
||||
# (
|
||||
# ('User-Name', '<username>'),
|
||||
|
@ -153,7 +109,7 @@ def authorize(
|
|||
tok = RadiusTokenResponse.parse_obj(
|
||||
loop.run_until_complete(_get_radius_token(username=user_id))
|
||||
)
|
||||
logging.debug("radius_token: %s", tok)
|
||||
logging.debug("radius information token: %s", tok)
|
||||
except NoMatchingEntries as error_message:
|
||||
logging.info(
|
||||
"kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user_id %s: %s",
|
||||
|
@ -164,8 +120,8 @@ def authorize(
|
|||
except Exception as error_message: # pylint: disable=broad-except
|
||||
logging.error("kanidm exception: %s, %s", type(error_message), error_message)
|
||||
if tok is None:
|
||||
logging.info("kanidm RLM_MODULE_NOTFOUND due to no auth token")
|
||||
return radiusd.RLM_MODULE_NOTFOUND
|
||||
logging.info("kanidm RLM_MODULE_REJECT - unable to retrieve radius information token")
|
||||
return radiusd.RLM_MODULE_REJECT
|
||||
|
||||
# Get values out of the token
|
||||
name = tok.name
|
||||
|
@ -174,14 +130,14 @@ def authorize(
|
|||
|
||||
# Are they in the required group?
|
||||
req_sat = False
|
||||
required_groups = kanidm_client.config.radius_required_groups
|
||||
for group in tok.groups:
|
||||
group_name = group.spn.split("@")[0]
|
||||
if group_name in kanidm_client.config.radius_required_groups:
|
||||
if group.uuid in required_groups or group.spn in required_groups:
|
||||
req_sat = True
|
||||
logging.info("User %s has a required group (%s)", name, group_name)
|
||||
logging.info("User %s has a required group (%s)", name, group.spn)
|
||||
if req_sat is not True:
|
||||
logging.info("User %s doesn't have a group from the required list.", name)
|
||||
return radiusd.RLM_MODULE_NOTFOUND
|
||||
return radiusd.RLM_MODULE_REJECT
|
||||
|
||||
# look up them in config for group vlan if possible.
|
||||
# TODO: work out the typing on this, WTF.
|
||||
|
@ -206,3 +162,21 @@ def authorize(
|
|||
|
||||
logging.info("OK! Returning details to radius for %s ...", name)
|
||||
return (radiusd.RLM_MODULE_OK, reply, config_object)
|
||||
|
||||
|
||||
|
||||
|
||||
def authenticate(
|
||||
acct: str,
|
||||
password: str,
|
||||
) -> Union[int, AuthStepPasswordResponse]:
|
||||
"""authenticate the RADIUS service account to Kanidm"""
|
||||
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
|
||||
logging.error("authenticate - %s:%s", acct, password)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(kanidm_client.check_token_valid())
|
||||
except Exception as error_message: # pylint: disable=broad-except
|
||||
logging.error("Failed to run kanidm.check_token_valid: %s", error_message)
|
||||
return radiusd.RLM_MODULE_FAIL
|
||||
|
|
34
pykanidm/kanidm/radius/utils.py
Normal file
34
pykanidm/kanidm/radius/utils.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
""" class utils """
|
||||
|
||||
from typing import Optional
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .. import KanidmClient
|
||||
from ..types import RadiusTokenGroup
|
||||
|
||||
def check_vlan(
|
||||
acc: int,
|
||||
group: RadiusTokenGroup,
|
||||
kanidm_client: Optional[KanidmClient] = None,
|
||||
) -> int:
|
||||
"""checks if a vlan is in the config,
|
||||
|
||||
acc is the default vlan
|
||||
"""
|
||||
logging.debug("acc=%s", acc)
|
||||
if kanidm_client is None:
|
||||
if "KANIDM_CONFIG_FILE" in os.environ:
|
||||
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
|
||||
else:
|
||||
raise ValueError("Need to pass this a kanidm_client")
|
||||
|
||||
for radius_group in kanidm_client.config.radius_groups:
|
||||
logging.debug(
|
||||
"Checking vlan group '%s' against user group %s", radius_group.spn, group.spn
|
||||
)
|
||||
if radius_group.spn == group.spn:
|
||||
logging.info("returning new vlan: %s", radius_group.vlan)
|
||||
return radius_group.vlan
|
||||
logging.debug("returning already set vlan: %s", acc)
|
||||
return acc
|
|
@ -93,7 +93,7 @@ class AuthStepPasswordResponse(BaseModel):
|
|||
class RadiusGroup(BaseModel):
|
||||
"""group for kanidm radius"""
|
||||
|
||||
name: str
|
||||
spn: str
|
||||
vlan: int
|
||||
|
||||
@validator("vlan")
|
||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
|||
from kanidm import KanidmClient
|
||||
from kanidm.types import KanidmClientConfig, RadiusTokenGroup
|
||||
|
||||
from kanidm.radius import check_vlan
|
||||
from kanidm.radius.utils import check_vlan
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
@ -18,8 +18,8 @@ async def test_check_vlan(event_loop: Any) -> None:
|
|||
"""
|
||||
uri='https://kanidm.example.com'
|
||||
radius_groups = [
|
||||
{ name = "crabz", "vlan" = 1234 },
|
||||
{ name = "hello world", "vlan" = 12345 },
|
||||
{ spn = "crabz@example.com", "vlan" = 1234 },
|
||||
{ spn = "hello@world", "vlan" = 12345 },
|
||||
]
|
||||
"""
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ async def test_check_vlan(event_loop: Any) -> None:
|
|||
assert (
|
||||
check_vlan(
|
||||
acc=12345678,
|
||||
group=RadiusTokenGroup(spn="crabz@domain.com", uuid="crabz"),
|
||||
group=RadiusTokenGroup(spn="crabz@example.com", uuid="crabz"),
|
||||
kanidm_client=kanidm_client,
|
||||
)
|
||||
== 1234
|
||||
|
|
|
@ -10,7 +10,7 @@ from kanidm.types import KanidmClientConfig
|
|||
from kanidm.utils import load_config
|
||||
|
||||
|
||||
EXAMPLE_CONFIG_FILE = "../../kanidm_rlm_python/examples/config"
|
||||
EXAMPLE_CONFIG_FILE = "../examples/config"
|
||||
|
||||
|
||||
def test_load_config_file() -> None:
|
||||
|
@ -30,7 +30,7 @@ def test_radius_groups() -> None:
|
|||
|
||||
config_toml = """
|
||||
radius_groups = [
|
||||
{ name = "hello world", "vlan" = 1234 },
|
||||
{ spn = "hello world", "vlan" = 1234 },
|
||||
]
|
||||
|
||||
"""
|
||||
|
@ -38,8 +38,8 @@ radius_groups = [
|
|||
print(config_parsed)
|
||||
kanidm_config = KanidmClientConfig.parse_obj(config_parsed)
|
||||
for group in kanidm_config.radius_groups:
|
||||
print(group.name)
|
||||
assert group.name == "hello world"
|
||||
print(group.spn)
|
||||
assert group.spn == "hello world"
|
||||
|
||||
|
||||
def test_radius_clients() -> None:
|
||||
|
|
|
@ -22,7 +22,7 @@ async def test_radius_call(client_configfile: KanidmClient) -> None:
|
|||
print("Doing auth_init using token")
|
||||
|
||||
if client_configfile.config.auth_token is None:
|
||||
raise ValueError("This path shouldn't be possible in the test!")
|
||||
pytest.skip("You can't test auth if you don't have an auth_token in ~/.config/kanidm")
|
||||
result = await client_configfile.get_radius_token(RADIUS_TEST_USER)
|
||||
|
||||
print(f"{result=}")
|
||||
|
|
|
@ -33,13 +33,13 @@ def test_radiusgroup_vlan_zero() -> None:
|
|||
|
||||
def test_radiusgroup_vlan_4096() -> None:
|
||||
"""tests RadiusGroup's vlan validator"""
|
||||
assert RadiusGroup(vlan=4096, name="crabzrool")
|
||||
assert RadiusGroup(vlan=4096, spn="crabzrool@foo")
|
||||
|
||||
|
||||
def test_radiusgroup_vlan_no_name() -> None:
|
||||
"""tests RadiusGroup's vlan validator"""
|
||||
with pytest.raises(
|
||||
pydantic.error_wrappers.ValidationError, match="name\n.*field required"
|
||||
pydantic.error_wrappers.ValidationError, match="spn\n.*field required"
|
||||
):
|
||||
RadiusGroup(
|
||||
vlan=4096,
|
||||
|
|
Loading…
Reference in a new issue