RADIUS container fixes (#1424)

This commit is contained in:
James Hodgkinson 2023-03-07 11:50:45 +10:00 committed by GitHub
parent 56a05223b4
commit 5573ab9224
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 176 additions and 57 deletions

View file

@ -110,6 +110,7 @@ codespell:
codespell -c \ codespell -c \
-L crate,unexpect,Pres,pres,ACI,aci,te,ue \ -L crate,unexpect,Pres,pres,ACI,aci,te,ue \
--skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache' \ --skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache' \
--skip='./book/book/*' \
--skip='./docs/*,./.git' \ --skip='./docs/*,./.git' \
--skip='./server/web_ui/src/external,./server/web_ui/pkg/external' \ --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' --skip='./server/lib/src/constants/system_config.rs,./pykanidm/site,./server/lib/src/constants/*.json'

View file

@ -28,7 +28,7 @@ radius_required_groups = [
] ]
# A mapping between Kanidm groups and VLANS # A mapping between Kanidm groups and VLANS
radius_groups = [ 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 # The default VLAN if the user does not fit into another group

View file

@ -16,6 +16,7 @@ impl KanidmClient {
.await .await
} }
/// Handles creating a service account
pub async fn idm_service_account_create( pub async fn idm_service_account_create(
&self, &self,
name: &str, name: &str,
@ -199,6 +200,7 @@ impl KanidmClient {
&self, &self,
id: &str, id: &str,
) -> Result<Vec<ApiToken>, ClientError> { ) -> 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()) self.perform_get_request(format!("/v1/service_account/{}/_api_token", id).as_str())
.await .await
} }

View file

@ -1,5 +1,6 @@
""" kanidm RADIUS module """ """ kanidm RADIUS module """
import asyncio import asyncio
from aiohttp.client_exceptions import ClientConnectorError
from functools import reduce from functools import reduce
import json import json
import logging import logging
@ -117,8 +118,12 @@ def authorize(
error_message, error_message,
) )
return radiusd.RLM_MODULE_NOTFOUND 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 except Exception as error_message: # pylint: disable=broad-except
logging.error("kanidm exception: %s, %s", type(error_message), error_message) logging.error("kanidm exception: %s, %s", type(error_message), error_message)
return radiusd.RLM_MODULE_FAIL
if tok is None: if tok is None:
logging.info( logging.info(
"kanidm RLM_MODULE_REJECT - unable to retrieve radius information token" "kanidm RLM_MODULE_REJECT - unable to retrieve radius information token"

View file

@ -1,33 +1,22 @@
ARG BASE_IMAGE=opensuse/tumbleweed:latest FROM freeradius/freeradius-server: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
EXPOSE 1812 1813 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 RUN apt-get update && apt-get install -y \
# up in the layers. freeradius-utils \
RUN \
--mount=type=cache,id=zypp,target=/var/cache/zypp \
zypper install -y \
freeradius-client \
freeradius-server \
freeradius-server-python3 \
freeradius-server-utils \
hostname \ hostname \
python310 \ python3 \
python310-devel \ python3-pip \
python310-pip \ python-is-python3 \
timezone \ tzdata \
iproute2 \ iproute2 \
iputils \ iputils-ping iputils-tracepath \
openssl \ openssl \
curl curl && apt-get clean
ADD rlm_python/mods-available/ /etc/raddb/mods-available/ ADD rlm_python/mods-available/ /etc/raddb/mods-available/
COPY rlm_python/sites-available/ /etc/raddb/sites-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 ln -s /etc/raddb/sites-available/check-eap-tls /etc/raddb/sites-enabled/check-eap-tls
# disable auth via methods we don't support! # disable auth via methods we don't support!
RUN rm /etc/raddb/mods-available/sql && \ # RUN rm /etc/raddb/mods-available/sql && \
rm /etc/raddb/mods-enabled/{passwd,totp} # rm /etc/raddb/mods-enabled/{passwd,totp}
# Allows the radiusd user to write to the directory # 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 775 /etc/raddb/certs && \
chmod 640 /etc/raddb/clients.conf 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 LD_PRELOAD=/usr/lib64/libpython3.so
ENV KANIDM_CONFIG_FILE="/data/kanidm" 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" ] CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ]

View file

@ -123,6 +123,19 @@ def kill_radius(
proc.wait() 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: def run_radiusd() -> None:
""" run the server """ """ run the server """
@ -131,7 +144,7 @@ def run_radiusd() -> None:
else: else:
cmd_args = [ "-f", "-l", "stdout" ] cmd_args = [ "-f", "-l", "stdout" ]
with subprocess.Popen( with subprocess.Popen(
["/usr/sbin/radiusd"] + cmd_args, [find_freeradius_bin()] + cmd_args,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
) as proc: ) as proc:
# print(proc, file=sys.stderr) # print(proc, file=sys.stderr)

View file

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
set -x
if [ -z "${IMAGE}" ]; then if [ -z "${IMAGE}" ]; then
IMAGE="kanidm/radius:devel" IMAGE="kanidm/radius:devel"
@ -21,5 +22,6 @@ docker run --rm -it \
--name radiusd \ --name radiusd \
-v /tmp/kanidm/:/data/ \ -v /tmp/kanidm/:/data/ \
-v /tmp/kanidm/:/tmp/kanidm/ \ -v /tmp/kanidm/:/tmp/kanidm/ \
-v /tmp/kanidm/:/certs/ \
-v "${CONFIG_FILE}:/data/kanidm" \ -v "${CONFIG_FILE}:/data/kanidm" \
${IMAGE} $@ "${IMAGE}" $@

53
rlm_python/run_test.sh Executable file
View 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

View file

@ -80,7 +80,7 @@ impl SessionConsistency {
entry.remove_avas("user_auth_token_session", invalidate); 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") let expired: Option<BTreeSet<_>> = entry.get_ava_as_session_map("user_auth_token_session")
.map(|sessions| { .map(|sessions| {
sessions.iter().filter_map(|(session_id, session)| { sessions.iter().filter_map(|(session_id, session)| {

View file

@ -364,8 +364,8 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
.await .await
.expect("Unable to decode OidcToken from userinfo"); .expect("Unable to decode OidcToken from userinfo");
eprintln!("{userinfo:?}"); eprintln!("userinfo {userinfo:?}");
eprintln!("{oidc:?}"); eprintln!("oidc {oidc:?}");
assert!(userinfo == oidc); assert!(userinfo == oidc);
} }

View file

@ -36,7 +36,7 @@ gloo = { workspace = true }
gloo-net = { workspace = true } gloo-net = { workspace = true }
js-sys = { workspace = true } js-sys = { workspace = true }
kanidm_proto = { workspace = true, features = ["wasm"] } 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 = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde-wasm-bindgen = { workspace = true } serde-wasm-bindgen = { workspace = true }

View file

@ -24,7 +24,12 @@ impl GroupOpt {
GroupOpt::List(copt) => { GroupOpt::List(copt) => {
let client = copt.to_client().await; let client = copt.to_client().await;
match client.idm_group_list().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), Err(e) => error!("Error -> {:?}", e),
} }
} }
@ -32,7 +37,12 @@ impl GroupOpt {
let client = gcopt.copt.to_client().await; let client = gcopt.copt.to_client().await;
// idm_group_get // idm_group_get
match client.idm_group_get(gcopt.name.as_str()).await { 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()), Ok(None) => warn!("No matching group '{}'", gcopt.name.as_str()),
Err(e) => error!("Error -> {:?}", e), Err(e) => error!("Error -> {:?}", e),
} }
@ -40,7 +50,9 @@ impl GroupOpt {
GroupOpt::Create(gcopt) => { GroupOpt::Create(gcopt) => {
let client = gcopt.copt.to_client().await; let client = gcopt.copt.to_client().await;
match client.idm_group_create(gcopt.name.as_str()).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()), Ok(_) => println!("Successfully created group '{}'", gcopt.name.as_str()),
} }
} }

View file

@ -315,14 +315,23 @@ impl PersonOpt {
} }
PersonOpt::Create(acopt) => { PersonOpt::Create(acopt) => {
let client = acopt.copt.to_client().await; let client = acopt.copt.to_client().await;
if let Err(e) = client match client
.idm_person_account_create( .idm_person_account_create(
acopt.aopts.account_id.as_str(), acopt.aopts.account_id.as_str(),
acopt.display_name.as_str(), acopt.display_name.as_str(),
) )
.await .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 { PersonOpt::Validity { commands } => match commands {

View file

@ -118,7 +118,7 @@ impl ServiceAccountOpt {
Some(odt) Some(odt)
} }
Err(e) => { Err(e) => {
error!("Error -> {:?}", e); error!("Error parsing expiry (input: {t:?}) -> {:?}", e);
return; return;
} }
} }
@ -137,10 +137,23 @@ impl ServiceAccountOpt {
) )
.await .await
{ {
Ok(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!("Success: This token will only be displayed ONCE");
println!("{}", new_token) println!("{}", new_token)
} }
},
Err(e) => { Err(e) => {
error!("Error generating service account api token -> {:?}", e); error!("Error generating service account api token -> {:?}", e);
} }

View file

@ -142,11 +142,21 @@ impl LoginOpt {
self.copt.debug self.copt.debug
} }
async fn do_password(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> { async fn do_password(
let password = rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| { &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); error!("Failed to create password prompt -- {:?}", e);
std::process::exit(1); std::process::exit(1);
}); }),
};
client.auth_step_password(password.as_str()).await client.auth_step_password(password.as_str()).await
} }
@ -228,9 +238,13 @@ impl LoginOpt {
pub async fn exec(&self) { pub async fn exec(&self) {
let mut client = self.copt.to_unauth_client(); let mut client = self.copt.to_unauth_client();
let username = match self.copt.username.as_deref() {
// TODO: remove this anon, nobody should do default anonymous Some(val) => val,
let username = self.copt.username.as_deref().unwrap_or("anonymous"); None => {
error!("Please specify a username with -D <USERNAME> to login.");
std::process::exit(1);
}
};
// What auth mechanisms exist? // What auth mechanisms exist?
let mechs: Vec<_> = client let mechs: Vec<_> = client
@ -313,7 +327,7 @@ impl LoginOpt {
let res = match choice { let res = match choice {
AuthAllowed::Anonymous => client.auth_step_anonymous().await, 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::BackupCode => self.do_backup_code(&mut client).await,
AuthAllowed::Totp => self.do_totp(&mut client).await, AuthAllowed::Totp => self.do_totp(&mut client).await,
AuthAllowed::Passkey(chal) => self.do_passkey(&mut client, chal.clone()).await, AuthAllowed::Passkey(chal) => self.do_passkey(&mut client, chal.clone()).await,

View file

@ -28,6 +28,9 @@ pub struct CommonOpt {
/// Path to a CA certificate file /// Path to a CA certificate file
#[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")] #[clap(parse(from_os_str), short = 'C', long = "ca", env = "KANIDM_CA_PATH")]
pub ca_path: Option<PathBuf>, 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)] #[derive(Debug, Args)]
@ -499,8 +502,9 @@ pub enum RecycleOpt {
pub struct LoginOpt { pub struct LoginOpt {
#[clap(flatten)] #[clap(flatten)]
copt: CommonOpt, copt: CommonOpt,
#[clap(short, long)] #[clap(short, long, env="KANIDM_PASSWORD", hide=true)]
webauthn: bool, /// Supply a password to the login option
password: Option<String>,
} }
#[derive(Debug, Args)] #[derive(Debug, Args)]