mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
RADIUS container fixes (#1424)
This commit is contained in:
parent
56a05223b4
commit
5573ab9224
1
Makefile
1
Makefile
|
@ -110,6 +110,7 @@ codespell:
|
|||
codespell -c \
|
||||
-L crate,unexpect,Pres,pres,ACI,aci,te,ue \
|
||||
--skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache' \
|
||||
--skip='./book/book/*' \
|
||||
--skip='./docs/*,./.git' \
|
||||
--skip='./server/web_ui/src/external,./server/web_ui/pkg/external' \
|
||||
--skip='./server/lib/src/constants/system_config.rs,./pykanidm/site,./server/lib/src/constants/*.json'
|
||||
|
|
|
@ -28,7 +28,7 @@ radius_required_groups = [
|
|||
]
|
||||
# A mapping between Kanidm groups and VLANS
|
||||
radius_groups = [
|
||||
{ name = "radius_access_allowed", vlan = 10 },
|
||||
{ spn = "radius_access_allowed", vlan = 10 },
|
||||
]
|
||||
|
||||
# The default VLAN if the user does not fit into another group
|
||||
|
|
|
@ -16,6 +16,7 @@ impl KanidmClient {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Handles creating a service account
|
||||
pub async fn idm_service_account_create(
|
||||
&self,
|
||||
name: &str,
|
||||
|
@ -199,6 +200,7 @@ impl KanidmClient {
|
|||
&self,
|
||||
id: &str,
|
||||
) -> Result<Vec<ApiToken>, ClientError> {
|
||||
// This ends up at [kanidmd_core::actors::v1_write::QueryServerWriteV1::handle_service_account_api_token_generate]
|
||||
self.perform_get_request(format!("/v1/service_account/{}/_api_token", id).as_str())
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" kanidm RADIUS module """
|
||||
import asyncio
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from functools import reduce
|
||||
import json
|
||||
import logging
|
||||
|
@ -117,8 +118,12 @@ def authorize(
|
|||
error_message,
|
||||
)
|
||||
return radiusd.RLM_MODULE_NOTFOUND
|
||||
except ClientConnectorError as client_error:
|
||||
logging.error("kanidm client connector error in http layer: %s", client_error)
|
||||
return radiusd.RLM_MODULE_FAIL
|
||||
except Exception as error_message: # pylint: disable=broad-except
|
||||
logging.error("kanidm exception: %s, %s", type(error_message), error_message)
|
||||
return radiusd.RLM_MODULE_FAIL
|
||||
if tok is None:
|
||||
logging.info(
|
||||
"kanidm RLM_MODULE_REJECT - unable to retrieve radius information token"
|
||||
|
|
|
@ -1,33 +1,22 @@
|
|||
ARG BASE_IMAGE=opensuse/tumbleweed:latest
|
||||
FROM ${BASE_IMAGE} AS repos
|
||||
RUN \
|
||||
--mount=type=cache,id=zypp,target=/var/cache/zypp \
|
||||
zypper mr -k repo-oss && \
|
||||
zypper mr -k repo-update
|
||||
|
||||
# ======================
|
||||
FROM repos
|
||||
|
||||
FROM freeradius/freeradius-server:latest
|
||||
EXPOSE 1812 1813
|
||||
ARG RADIUS_USER=freerad
|
||||
ARG TZ=Etc/UTC
|
||||
ENV TZ=$TZ
|
||||
# These all need to be on one line else the cache ends up in the layers.
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# These all need to be on one line else the rpm cache ends
|
||||
# up in the layers.
|
||||
RUN \
|
||||
--mount=type=cache,id=zypp,target=/var/cache/zypp \
|
||||
zypper install -y \
|
||||
freeradius-client \
|
||||
freeradius-server \
|
||||
freeradius-server-python3 \
|
||||
freeradius-server-utils \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
freeradius-utils \
|
||||
hostname \
|
||||
python310 \
|
||||
python310-devel \
|
||||
python310-pip \
|
||||
timezone \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python-is-python3 \
|
||||
tzdata \
|
||||
iproute2 \
|
||||
iputils \
|
||||
iputils-ping iputils-tracepath \
|
||||
openssl \
|
||||
curl
|
||||
curl && apt-get clean
|
||||
|
||||
ADD rlm_python/mods-available/ /etc/raddb/mods-available/
|
||||
COPY rlm_python/sites-available/ /etc/raddb/sites-available/
|
||||
|
@ -40,11 +29,12 @@ RUN ln -s /etc/raddb/mods-available/python3 /etc/raddb/mods-enabled/python3 && \
|
|||
ln -s /etc/raddb/sites-available/check-eap-tls /etc/raddb/sites-enabled/check-eap-tls
|
||||
|
||||
# disable auth via methods we don't support!
|
||||
RUN rm /etc/raddb/mods-available/sql && \
|
||||
rm /etc/raddb/mods-enabled/{passwd,totp}
|
||||
# RUN rm /etc/raddb/mods-available/sql && \
|
||||
# rm /etc/raddb/mods-enabled/{passwd,totp}
|
||||
|
||||
|
||||
# Allows the radiusd user to write to the directory
|
||||
RUN chown -R radiusd: /etc/raddb && \
|
||||
RUN chown -R $RADIUS_USER. /etc/raddb && \
|
||||
chmod 775 /etc/raddb/certs && \
|
||||
chmod 640 /etc/raddb/clients.conf
|
||||
|
||||
|
@ -60,6 +50,7 @@ COPY rlm_python/radius_entrypoint.py /radius_entrypoint.py
|
|||
ENV LD_PRELOAD=/usr/lib64/libpython3.so
|
||||
ENV KANIDM_CONFIG_FILE="/data/kanidm"
|
||||
|
||||
USER radiusd
|
||||
RUN chmod a+r /etc/raddb/certs/ -R
|
||||
USER $RADIUS_USER
|
||||
|
||||
CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ]
|
||||
|
|
|
@ -123,6 +123,19 @@ def kill_radius(
|
|||
|
||||
proc.wait()
|
||||
|
||||
def find_freeradius_bin() -> str:
|
||||
""" finds the binary """
|
||||
binary_paths = [
|
||||
"/usr/sbin/radiusd",
|
||||
"/usr/sbin/freeradius",
|
||||
]
|
||||
for path in binary_paths:
|
||||
if Path(path).exists():
|
||||
return path
|
||||
lookedin = ", ".join(binary_paths)
|
||||
print(f"Failed to find FreeRADIUS binary, looked in {lookedin}")
|
||||
sys.exit(1)
|
||||
|
||||
def run_radiusd() -> None:
|
||||
""" run the server """
|
||||
|
||||
|
@ -131,7 +144,7 @@ def run_radiusd() -> None:
|
|||
else:
|
||||
cmd_args = [ "-f", "-l", "stdout" ]
|
||||
with subprocess.Popen(
|
||||
["/usr/sbin/radiusd"] + cmd_args,
|
||||
[find_freeradius_bin()] + cmd_args,
|
||||
stderr=subprocess.STDOUT,
|
||||
) as proc:
|
||||
# print(proc, file=sys.stderr)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#!/bin/bash
|
||||
set -x
|
||||
|
||||
if [ -z "${IMAGE}" ]; then
|
||||
IMAGE="kanidm/radius:devel"
|
||||
|
@ -21,5 +22,6 @@ docker run --rm -it \
|
|||
--name radiusd \
|
||||
-v /tmp/kanidm/:/data/ \
|
||||
-v /tmp/kanidm/:/tmp/kanidm/ \
|
||||
-v /tmp/kanidm/:/certs/ \
|
||||
-v "${CONFIG_FILE}:/data/kanidm" \
|
||||
${IMAGE} $@
|
||||
"${IMAGE}" $@
|
||||
|
|
53
rlm_python/run_test.sh
Executable file
53
rlm_python/run_test.sh
Executable file
|
@ -0,0 +1,53 @@
|
|||
#!/bin/bash
|
||||
|
||||
# set -e
|
||||
|
||||
TEST_RADIUS_USER="test_radius_user"
|
||||
RADIUS_GROUP="radius_access_allowed"
|
||||
|
||||
#shellcheck disable=SC2162
|
||||
read -p "Enter idm_admin password: " KANIDM_PASSWORD
|
||||
|
||||
export KANIDM_PASSWORD
|
||||
cargo run --bin kanidm login --name idm_admin
|
||||
unset KANIDM_PASSWORD
|
||||
|
||||
GROUP_CREATE_OUTPUT="$(KANIDM_NAME=idm_admin cargo run --bin kanidm group create "${RADIUS_GROUP}" 2>&1)"
|
||||
GROUP_CREATE_RESULT="$(echo "${GROUP_CREATE_OUTPUT}" | grep -c -E '(Successfully created|AttrUnique)')"
|
||||
|
||||
if [ "${GROUP_CREATE_RESULT}" -eq 1 ]; then
|
||||
echo "Group ${RADIUS_GROUP} created"
|
||||
else
|
||||
echo "Something failed during group creation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
echo "Creating RADIUS test user ${TEST_RADIUS_USER}"
|
||||
USER_CREATE_OUTPUT="$(KANIDM_NAME=idm_admin cargo run --bin kanidm service-account create "${TEST_RADIUS_USER}" "${TEST_RADIUS_USER}")"
|
||||
|
||||
USER_CREATE_RESULT="$(echo "${USER_CREATE_OUTPUT}" | grep -c -E '(Successfully created|AttrUnique)')"
|
||||
if [ "${USER_CREATE_RESULT}" -eq 1 ]; then
|
||||
echo "User ${TEST_RADIUS_USER} created"
|
||||
else
|
||||
echo "Something failed during service account creation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
echo "Creating API Token..."
|
||||
TOKEN_EXPIRY="$(date -v+1H +%Y-%m-%dT%H:%M:%S+10:00)"
|
||||
|
||||
RADIUS_TOKEN_RESULT="$(KANIDM_NAME=idm_admin cargo run --bin kanidm service-account api-token generate \
|
||||
"${TEST_RADIUS_USER}" radius "${TOKEN_EXPIRY}" \
|
||||
-o json)"
|
||||
RADIUS_TOKEN="$(echo "${RADIUS_TOKEN_RESULT}" | grep result | jq -r .result)"
|
||||
|
||||
if [ -z "${RADIUS_TOKEN}" ]; then
|
||||
echo "Couldn't find RADIUS token in output"
|
||||
echo "${RADIUS_TOKEN_RESULT}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Updating secret in config file"
|
||||
sed -i '' -e "s/^secret.*/secret = \"${RADIUS_TOKEN}\"/" ~/.config/kanidm
|
|
@ -80,7 +80,7 @@ impl SessionConsistency {
|
|||
entry.remove_avas("user_auth_token_session", invalidate);
|
||||
}
|
||||
|
||||
// * If a UAT is past it's expiry, remove it.
|
||||
// * If a UAT is past its expiry, remove it.
|
||||
let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map("user_auth_token_session")
|
||||
.map(|sessions| {
|
||||
sessions.iter().filter_map(|(session_id, session)| {
|
||||
|
|
|
@ -364,8 +364,8 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
.await
|
||||
.expect("Unable to decode OidcToken from userinfo");
|
||||
|
||||
eprintln!("{userinfo:?}");
|
||||
eprintln!("{oidc:?}");
|
||||
eprintln!("userinfo {userinfo:?}");
|
||||
eprintln!("oidc {oidc:?}");
|
||||
|
||||
assert!(userinfo == oidc);
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ gloo = { workspace = true }
|
|||
gloo-net = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
kanidm_proto = { workspace = true, features = ["wasm"] }
|
||||
qrcode = { workspace = true, default-features = false, features = ["svg"] }
|
||||
qrcode = { workspace = true, features = ["svg"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde-wasm-bindgen = { workspace = true }
|
||||
|
|
|
@ -24,7 +24,12 @@ impl GroupOpt {
|
|||
GroupOpt::List(copt) => {
|
||||
let client = copt.to_client().await;
|
||||
match client.idm_group_list().await {
|
||||
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
|
||||
Ok(r) => r.iter().for_each(|ent| match copt.output_mode.as_str() {
|
||||
"json" => {
|
||||
println!("{}", serde_json::to_string(&ent.attrs).unwrap());
|
||||
}
|
||||
_ => println!("{}", ent),
|
||||
}),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +37,12 @@ impl GroupOpt {
|
|||
let client = gcopt.copt.to_client().await;
|
||||
// idm_group_get
|
||||
match client.idm_group_get(gcopt.name.as_str()).await {
|
||||
Ok(Some(e)) => println!("{}", e),
|
||||
Ok(Some(e)) => match gcopt.copt.output_mode.as_str() {
|
||||
"json" => {
|
||||
println!("{}", serde_json::to_string(&e.attrs).unwrap());
|
||||
}
|
||||
_ => println!("{}", e),
|
||||
},
|
||||
Ok(None) => warn!("No matching group '{}'", gcopt.name.as_str()),
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
}
|
||||
|
@ -40,7 +50,9 @@ impl GroupOpt {
|
|||
GroupOpt::Create(gcopt) => {
|
||||
let client = gcopt.copt.to_client().await;
|
||||
match client.idm_group_create(gcopt.name.as_str()).await {
|
||||
Err(e) => error!("Error -> {:?}", e),
|
||||
Err(err) => {
|
||||
error!("Error -> {:?}", err)
|
||||
}
|
||||
Ok(_) => println!("Successfully created group '{}'", gcopt.name.as_str()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -315,14 +315,23 @@ impl PersonOpt {
|
|||
}
|
||||
PersonOpt::Create(acopt) => {
|
||||
let client = acopt.copt.to_client().await;
|
||||
if let Err(e) = client
|
||||
match client
|
||||
.idm_person_account_create(
|
||||
acopt.aopts.account_id.as_str(),
|
||||
acopt.display_name.as_str(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Error -> {:?}", e)
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Successfully created display_name=\"{}\" username={}>",
|
||||
acopt.display_name.as_str(),
|
||||
acopt.aopts.account_id.as_str(),
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error -> {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
PersonOpt::Validity { commands } => match commands {
|
||||
|
|
|
@ -118,7 +118,7 @@ impl ServiceAccountOpt {
|
|||
Some(odt)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error -> {:?}", e);
|
||||
error!("Error parsing expiry (input: {t:?}) -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -137,10 +137,23 @@ impl ServiceAccountOpt {
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(new_token) => {
|
||||
println!("Success: This token will only be displayed ONCE");
|
||||
println!("{}", new_token)
|
||||
}
|
||||
Ok(new_token) => match copt.output_mode.as_str() {
|
||||
"json" => {
|
||||
let message = AccountChangeMessage {
|
||||
output_mode: ConsoleOutputMode::JSON,
|
||||
action: "api-token generate".to_string(),
|
||||
result: new_token,
|
||||
status: kanidm_proto::messages::MessageStatus::Success,
|
||||
src_user: copt.username.clone().unwrap(),
|
||||
dest_user: aopts.account_id.clone(),
|
||||
};
|
||||
println!("{}", message.to_string());
|
||||
}
|
||||
_ => {
|
||||
println!("Success: This token will only be displayed ONCE");
|
||||
println!("{}", new_token)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Error generating service account api token -> {:?}", e);
|
||||
}
|
||||
|
|
|
@ -142,11 +142,21 @@ impl LoginOpt {
|
|||
self.copt.debug
|
||||
}
|
||||
|
||||
async fn do_password(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
|
||||
let password = rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
|
||||
error!("Failed to create password prompt -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
});
|
||||
async fn do_password(
|
||||
&self,
|
||||
client: &mut KanidmClient,
|
||||
password: &Option<String>,
|
||||
) -> Result<AuthResponse, ClientError> {
|
||||
let password = match password {
|
||||
Some(password) => {
|
||||
trace!("User provided password directly, don't need to prompt.");
|
||||
password.to_owned()
|
||||
}
|
||||
None => rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
|
||||
error!("Failed to create password prompt -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
}),
|
||||
};
|
||||
client.auth_step_password(password.as_str()).await
|
||||
}
|
||||
|
||||
|
@ -228,9 +238,13 @@ impl LoginOpt {
|
|||
|
||||
pub async fn exec(&self) {
|
||||
let mut client = self.copt.to_unauth_client();
|
||||
|
||||
// TODO: remove this anon, nobody should do default anonymous
|
||||
let username = self.copt.username.as_deref().unwrap_or("anonymous");
|
||||
let username = match self.copt.username.as_deref() {
|
||||
Some(val) => val,
|
||||
None => {
|
||||
error!("Please specify a username with -D <USERNAME> to login.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// What auth mechanisms exist?
|
||||
let mechs: Vec<_> = client
|
||||
|
@ -313,7 +327,7 @@ impl LoginOpt {
|
|||
|
||||
let res = match choice {
|
||||
AuthAllowed::Anonymous => client.auth_step_anonymous().await,
|
||||
AuthAllowed::Password => self.do_password(&mut client).await,
|
||||
AuthAllowed::Password => self.do_password(&mut client, &self.password).await,
|
||||
AuthAllowed::BackupCode => self.do_backup_code(&mut client).await,
|
||||
AuthAllowed::Totp => self.do_totp(&mut client).await,
|
||||
AuthAllowed::Passkey(chal) => self.do_passkey(&mut client, chal.clone()).await,
|
||||
|
|
|
@ -28,6 +28,9 @@ pub struct CommonOpt {
|
|||
/// Path to a CA certificate file
|
||||
#[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")]
|
||||
pub ca_path: Option<PathBuf>,
|
||||
/// Log format (still in very early development)
|
||||
#[clap(short, long = "output", env = "KANIDM_OUTPUT", default_value="text")]
|
||||
output_mode: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
@ -499,8 +502,9 @@ pub enum RecycleOpt {
|
|||
pub struct LoginOpt {
|
||||
#[clap(flatten)]
|
||||
copt: CommonOpt,
|
||||
#[clap(short, long)]
|
||||
webauthn: bool,
|
||||
#[clap(short, long, env="KANIDM_PASSWORD", hide=true)]
|
||||
/// Supply a password to the login option
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
|
|
Loading…
Reference in a new issue