diff --git a/Makefile b/Makefile index 0bd07edd2..4d4058f8e 100644 --- a/Makefile +++ b/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' diff --git a/examples/kanidm b/examples/kanidm index 7e36ab895..1a9b1a959 100644 --- a/examples/kanidm +++ b/examples/kanidm @@ -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 diff --git a/libs/client/src/service_account.rs b/libs/client/src/service_account.rs index 8a805efd0..36f649227 100644 --- a/libs/client/src/service_account.rs +++ b/libs/client/src/service_account.rs @@ -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, 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 } diff --git a/pykanidm/kanidm/radius/__init__.py b/pykanidm/kanidm/radius/__init__.py index e4ada5a17..57972ce02 100644 --- a/pykanidm/kanidm/radius/__init__.py +++ b/pykanidm/kanidm/radius/__init__.py @@ -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" diff --git a/rlm_python/Dockerfile b/rlm_python/Dockerfile index 981ee22e1..ecebec99d 100644 --- a/rlm_python/Dockerfile +++ b/rlm_python/Dockerfile @@ -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" ] diff --git a/rlm_python/radius_entrypoint.py b/rlm_python/radius_entrypoint.py index 4b2efd077..b881e7a39 100644 --- a/rlm_python/radius_entrypoint.py +++ b/rlm_python/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) diff --git a/rlm_python/run_radius_container.sh b/rlm_python/run_radius_container.sh index d18d4490a..d73e8ec07 100755 --- a/rlm_python/run_radius_container.sh +++ b/rlm_python/run_radius_container.sh @@ -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}" $@ diff --git a/rlm_python/run_test.sh b/rlm_python/run_test.sh new file mode 100755 index 000000000..1b511fa7f --- /dev/null +++ b/rlm_python/run_test.sh @@ -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 diff --git a/server/lib/src/plugins/session.rs b/server/lib/src/plugins/session.rs index 1d54bd51a..c11d123b5 100644 --- a/server/lib/src/plugins/session.rs +++ b/server/lib/src/plugins/session.rs @@ -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> = entry.get_ava_as_session_map("user_auth_token_session") .map(|sessions| { sessions.iter().filter_map(|(session_id, session)| { diff --git a/server/testkit/tests/oauth2_test.rs b/server/testkit/tests/oauth2_test.rs index 81c32ab98..31555ea63 100644 --- a/server/testkit/tests/oauth2_test.rs +++ b/server/testkit/tests/oauth2_test.rs @@ -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); } diff --git a/server/web_ui/Cargo.toml b/server/web_ui/Cargo.toml index 7a38d5a60..80f87c7e1 100644 --- a/server/web_ui/Cargo.toml +++ b/server/web_ui/Cargo.toml @@ -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 } diff --git a/tools/cli/src/cli/group.rs b/tools/cli/src/cli/group.rs index 5aa918dd0..c1b14b1e9 100644 --- a/tools/cli/src/cli/group.rs +++ b/tools/cli/src/cli/group.rs @@ -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()), } } diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs index bb645d680..6ceff378e 100644 --- a/tools/cli/src/cli/person.rs +++ b/tools/cli/src/cli/person.rs @@ -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 { diff --git a/tools/cli/src/cli/serviceaccount.rs b/tools/cli/src/cli/serviceaccount.rs index 3ee02d7a2..eb08b9d00 100644 --- a/tools/cli/src/cli/serviceaccount.rs +++ b/tools/cli/src/cli/serviceaccount.rs @@ -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); } diff --git a/tools/cli/src/cli/session.rs b/tools/cli/src/cli/session.rs index f71c87e68..080118b47 100644 --- a/tools/cli/src/cli/session.rs +++ b/tools/cli/src/cli/session.rs @@ -142,11 +142,21 @@ impl LoginOpt { self.copt.debug } - async fn do_password(&self, client: &mut KanidmClient) -> Result { - 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, + ) -> Result { + 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 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, diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs index 8484f3957..3507f4e6f 100644 --- a/tools/cli/src/opt/kanidm.rs +++ b/tools/cli/src/opt/kanidm.rs @@ -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, + /// 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, } #[derive(Debug, Args)]