Fix issues with radius (#1084)

This commit is contained in:
Firstyear 2022-10-02 11:28:58 +10:00 committed by GitHub
parent 2e9a94e703
commit aa9af0705c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 164 additions and 153 deletions

View file

@ -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 = [

View file

@ -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" ]

View file

@ -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(

View file

@ -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,

View file

@ -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,

View file

@ -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

View 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

View file

@ -93,7 +93,7 @@ class AuthStepPasswordResponse(BaseModel):
class RadiusGroup(BaseModel):
"""group for kanidm radius"""
name: str
spn: str
vlan: int
@validator("vlan")

View file

@ -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

View file

@ -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:

View file

@ -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=}")

View file

@ -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,