Converting from tide to axum (#1797)

* Starting to chase down testing
* commenting out unused/inactive endpoints, adding more tests
* clippyism
* making clippy happy v2
* testing when things are not right
* moar checkpoint
* splitting up testkit things a bit
* moving https -> tide
* mad lad be crabbin
* spawning like a frog
* something something different spawning
* woot it works ish
* more server things
* adding version header to requests
* adding kopid_middleware
* well that was supposed to be an hour... four later
* more nonsense
* carrying on with the conversion
* first pass through the conversion is DONE!
* less pub more better
* session storage works better, fixed some paths
* axum-csp version thing
* try a typedheader
* better openssl config things
* updating lockfile
* http2
* actually sending JSON when we say we will!
* just about to do something dumb
* flargl
* more yak shaving
* So many clippy-isms, fixing up a query handler bleep bloop
* So many clippy-isms, fixing up a query handler bleep bloop
* fmt
* all tests pass including basic web logins and nav
* so much clippyism
* stripping out old comments
* fmt
* commenty things
* stripping out tide
* updates
* de-tiding things
* fmt
* adding optional header matching ,thanks @cuberoot74088
* oauth2 stuff to match #1807 but in axum
* CLIPPY IS FINALLY SATED
* moving scim from /v1/scim to /scim
* one day clippy will make sense
* cleanups
* removing sketching middleware
* cleanup, strip a broken test endpoint (routemap), more clippy
* docs fmt
* pulling axum-csp from the wrong cargo.toml
* docs fmt
* fmt fixes
This commit is contained in:
James Hodgkinson 2023-07-05 22:26:39 +10:00 committed by GitHub
parent 17fa61ceeb
commit cc35654388
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 4704 additions and 3484 deletions

986
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,8 @@ repository = "https://github.com/kanidm/kanidm/"
argon2 = { version = "0.5.0", features = ["alloc"] }
async-recursion = "1.0.4"
async-trait = "^0.1.68"
axum = {version = "0.6.18", features = ["json", "http2", "macros", "tracing", "headers", "original-uri", "query", "form", "http2"]}
axum-csp = { version = "0.0.5" }
base32 = "^0.4.0"
base64 = "^0.21.0"
base64urlsafedata = "0.1.3"
@ -67,20 +69,19 @@ futures = "^0.3.28"
futures-concurrency = "^3.1.0"
futures-util = { version = "^0.3.21", features = ["sink"] }
gloo = "^0.8.1"
gloo-net = "0.3.0"
hashbrown = { version = "0.14.0", features = ["serde", "inline-more", "ahash"] }
hex = "^0.4.3"
http-types = "^2.12.0"
hyper = { version = "0.14.27", features = ["full"] }
hyper-tls = "0.5.0"
idlset = "^0.2.4"
# idlset = { path = "../idlset" }
js-sys = "^0.3.63"
kanidmd_core = { path = "./server/core" }
kanidmd_idm = { path = "./server/idm" }
kanidmd_lib = { path = "./server/lib" }
kanidmd_lib_macros = { path = "./server/lib-macros" }
kanidm_lib_crypto = { path = "./libs/crypto" }
kanidm_lib_file_permissions = { path = "./libs/file_permissions" }
kanidmd_testkit = { path = "./server/testkit" }
kanidm_client = { path = "./libs/client", version = "1.1.0-alpha.11" }
kanidm_proto = { path = "./proto", version = "1.1.0-alpha.11" }
kanidm_unix_int = { path = "./unix_integration" }
@ -113,7 +114,7 @@ qrcode = "^0.12.0"
quote = "1"
rand = "^0.8.5"
regex = "1.8.4"
reqwest = { version = "0.11.18", default-features = false, features=["cookies", "json", "gzip", "native-tls"] }
reqwest = { version = "0.11.18", default-features = false, features=["cookies", "json", "gzip", "native-tls", "native-tls-alpn"] }
rpassword = "^7.2.0"
rusqlite = "^0.28.0"
@ -133,10 +134,6 @@ smolset = "^1.3.1"
sshkeys = "^0.3.1"
syn = { version = "2.0.23", features = ["full"] }
testkit-macros = { path = "./server/testkit-macros" }
tide = "^0.16.0"
# Including brotli *very* slow, so don't do that. Including the "default" feature pulls a mime-type list from the internet on build, which isn't used.
tide-compress = { version="0.10.6", default-features = false, features = [ "gzip", "regex-check" ] }
tide-openssl = "^0.1.1"
time = { version = "^0.3.21", features = ["formatting", "local-offset"] }
tikv-jemallocator = "0.5"
@ -182,12 +179,5 @@ yew-router = "^0.17.0"
zxcvbn = "^2.2.2"
nonempty = "0.8.1"
# enshrinken the WASMs
[profile.release.package.kanidmd_web_ui]
# optimization over all codebase ( better optimization, slower build )
codegen-units = 1
# optimization for size ( more aggressive )
opt-level = 'z'
# link time optimization using using whole-program analysis
# lto = true

View file

@ -7,7 +7,7 @@ CONTAINER_BUILD_ARGS ?=
MARKDOWN_FORMAT_ARGS ?= --options-line-width=100
CONTAINER_TOOL ?= docker
BUILDKIT_PROGRESS ?= plain
TESTS ?=
BOOK_VERSION ?= master
.DEFAULT: help
@ -15,6 +15,11 @@ BOOK_VERSION ?= master
help:
@grep -E -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
.PHONY: run
run: ## Run the test/dev server
run:
cd server/daemon && ./run_insecure_dev_server.sh
.PHONY: buildx/kanidmd
buildx/kanidmd: ## Build multiarch kanidm server images and push to docker hub
buildx/kanidmd:
@ -259,3 +264,27 @@ webui: ## Build the WASM web frontend
.PHONY: webui/test
webui/test: ## Run wasm-pack test
cd server/web_ui && wasm-pack test --headless --chrome
.PHONY: rust/coverage
coverage/test: ## Run coverage tests
coverage/test:
LLVM_PROFILE_FILE="$(PWD)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test $(TESTS)
.PHONY: coverage/grcov
coverage/grcov: ## Run grcov
coverage/grcov:
rm -rf ./target/coverage/html
grcov . --binary-path ./target/debug/deps/ \
-s . \
-t html \
--branch \
--ignore-not-existing \
--ignore '../*' \
--ignore "/*" \
--ignore "target/*" \
-o target/coverage/html
.PHONY: coverage
coverage: ## Run all the coverage tests
coverage: coverage/test coverage/grcov
echo "Coverage report is in ./target/coverage/html/index.html"

View file

@ -1,6 +1,6 @@
## Getting Started (for Developers)
# Getting Started (for Developers)
### Setup the Server
## Setup the Server
It's important before you start trying to write code and contribute that you understand what Kanidm
does and its goals.
@ -8,20 +8,21 @@ does and its goals.
An important first step is to [install the server](installing_the_server.md) so if you have not done
that yet, go and try that now! 😄
### Setting up your Machine
## Setting up your Machine
Each operating system has different steps required to configure and build Kanidm.
#### MacOS
### MacOS
A prerequisite is [Apple Xcode](https://apps.apple.com/au/app/xcode/id497799835?mt=12) for access to
git and compiler tools. You should install this first.
You will need [rustup](https://rustup.rs/) to install a Rust toolchain.
To build the Web UI you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/) (`cargo install wasm-pack`).
To build the Web UI you'll need [wasm-pack](https://rustwasm.github.io/wasm-pack/)
(`cargo install wasm-pack`).
#### SUSE / OpenSUSE
### SUSE / OpenSUSE
You will need to install rustup and our build dependencies with:
@ -31,7 +32,7 @@ zypper in rustup git libudev-devel sqlite3-devel libopenssl-3-devel
You can then use rustup to complete the setup of the toolchain.
#### Fedora
### Fedora
You will need [rustup](https://rustup.rs/) to install a Rust toolchain.
@ -47,7 +48,7 @@ Building the Web UI requires additional packages:
perl-FindBin perl-File-Compare
```
#### Ubuntu
### Ubuntu
You need [rustup](https://rustup.rs/) to install a Rust toolchain.
@ -59,7 +60,7 @@ sudo apt-get install libsqlite3-dev libudev-dev libssl-dev pkg-config libpam0g-d
Tested with Ubuntu 20.04 and 22.04.
#### Windows
### Windows
<!-- deno-fmt-ignore-start -->
@ -94,7 +95,7 @@ vcpkg install openssl:x64-windows-static-md
There's a powershell script in the root directory of the repository which, in concert with `openssl`
will generate a config file and certs for testing.
### Getting the Source Code
## Getting the Source Code
### Get Involved
@ -157,7 +158,7 @@ git pull
git branch -D <feature-branch-name>
```
#### Rebasing
### Rebasing
If you are asked to rebase your change, follow these steps:
@ -196,25 +197,25 @@ cd book
mdbook serve
```
### Designs
## Designs
See the "Design Documents" section of this book.
### Rust Documentation
## Rust Documentation
A list of links to the library documentation is at
[kanidm.com/documentation](https://kanidm.com/documentation/).
### Advanced
## Advanced
#### Minimum Supported Rust Version
### Minimum Supported Rust Version
The MSRV is specified in the package `Cargo.toml` files.
We tend to be quite proactive in updating this to recent rust versions so we are open to increasing
this value if required!
#### Build Profiles
### Build Profiles
Build profiles allow us to change the operation of Kanidm during it's compilation for development or
release on various platforms. By default the "developer" profile is used that assumes the correct
@ -230,7 +231,7 @@ For example, this will set the CPU flags to "none" and the location for the Web
KANIDM_BUILD_PROFILE=release_suse_generic cargo build --release --bin kanidmd
```
#### Building the Web UI
### Building the Web UI
**NOTE:** There is a pre-packaged version of the Web UI at `/server/web_ui/pkg/`, which can be used
directly. This means you don't need to build the Web UI yourself.
@ -253,7 +254,7 @@ To build for release, run `build_wasm_release.sh`.
The "developer" profile for kanidmd will automatically use the pkg output in this folder.
#### Development Server for Interactive Testing
### Development Server for Interactive Testing
Especially if you wish to develop the WebUI then the ability to run the server from the source tree
is critical.
@ -294,7 +295,7 @@ cargo run --bin kanidm -- self whoami -H https://localhost:8443 -D admin -C /tmp
You may find it easier to modify `~/.config/kanidm` per the
[book client tools section](client_tools.md) for extended administration locally.
#### Raw actions
### Raw actions
<!-- deno-fmt-ignore-start -->
@ -326,7 +327,7 @@ kanidm raw search -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"eq
kanidm raw delete -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "test_account_delete_me"]}'
```
#### Build a Kanidm Container
### Build a Kanidm Container
Build a container with the current branch using:
@ -348,7 +349,7 @@ The following environment variables control the build:
| `CONTAINER_TOOL` | Use an alternative container build tool. | `docker` |
| `BOOK_VERSION` | Sets version used when building the documentation book. | `master` |
##### Container Build Examples
#### Container Build Examples
Build a `kanidm` container using `podman`:
@ -362,7 +363,7 @@ Build a `kanidm` container and use a redis build cache:
CONTAINER_BUILD_ARGS='--build-arg "SCCACHE_REDIS=redis://redis.dev.blackhats.net.au:6379"' make build/kanidmd
```
##### Automatically Built Containers
#### Automatically Built Containers
To speed up testing across platforms, we're leveraging GitHub actions to build containers for test
use.

View file

@ -0,0 +1,7 @@
# Content-Security-Policy Headers
These are required on any path which browser-based clients access.
- `/`
- `/ui/`
- `/pkg/`

View file

@ -62,6 +62,8 @@ pub enum ClientError {
JsonDecode(reqwest::Error, String),
JsonEncode(SerdeJsonError),
SystemError,
ConfigParseIssue(String),
CertParseIssue(String),
}
#[derive(Debug, Deserialize, Serialize)]
@ -136,7 +138,7 @@ impl KanidmClientBuilder {
}
}
fn parse_certificate(ca_path: &str) -> Result<reqwest::Certificate, ()> {
fn parse_certificate(ca_path: &str) -> Result<reqwest::Certificate, ClientError> {
let mut buf = Vec::new();
// Is the CA secure?
#[cfg(target_family = "windows")]
@ -145,7 +147,10 @@ impl KanidmClientBuilder {
#[cfg(target_family = "unix")]
{
let path = Path::new(ca_path);
let ca_meta = read_file_metadata(&path)?;
let ca_meta = read_file_metadata(&path).map_err(|e| {
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
trace!("uid:gid {}:{}", ca_meta.uid(), ca_meta.gid());
@ -165,17 +170,20 @@ impl KanidmClientBuilder {
// TODO #725: Handle these errors better, or at least provide diagnostics - this currently fails silently
let mut f = File::open(ca_path).map_err(|e| {
error!(?e);
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
f.read_to_end(&mut buf).map_err(|e| {
error!(?e);
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
reqwest::Certificate::from_pem(&buf).map_err(|e| {
error!(?e);
error!("{:?}", e);
ClientError::CertParseIssue(format!("{:?}", e))
})
}
fn apply_config_options(self, kcc: KanidmClientConfig) -> Result<Self, ()> {
fn apply_config_options(self, kcc: KanidmClientConfig) -> Result<Self, ClientError> {
let KanidmClientBuilder {
address,
verify_ca,
@ -213,7 +221,7 @@ impl KanidmClientBuilder {
pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
self,
config_path: P,
) -> Result<Self, ()> {
) -> Result<Self, ClientError> {
debug!("Attempting to load configuration from {:#?}", &config_path);
// We have to check the .exists case manually, because there are some weird overlayfs
@ -257,11 +265,15 @@ impl KanidmClientBuilder {
};
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| error!("{:?}", e))?;
f.read_to_string(&mut contents).map_err(|e| {
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
let config: KanidmClientConfig =
toml::from_str(contents.as_str()).map_err(|e| error!("{:?}", e))?;
let config: KanidmClientConfig = toml::from_str(contents.as_str()).map_err(|e| {
error!("{:?}", e);
ClientError::ConfigParseIssue(format!("{:?}", e))
})?;
self.apply_config_options(config)
}
@ -324,9 +336,12 @@ impl KanidmClientBuilder {
}
#[allow(clippy::result_unit_err)]
pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ()> {
pub fn add_root_certificate_filepath(self, ca_path: &str) -> Result<Self, ClientError> {
//Okay we have a ca to add. Let's read it in and setup.
let ca = Self::parse_certificate(ca_path)?;
let ca = Self::parse_certificate(ca_path).map_err(|e| {
error!("{:?}", e);
ClientError::CertParseIssue(format!("{:?}", e))
})?;
Ok(KanidmClientBuilder {
address: self.address,
@ -537,7 +552,7 @@ impl KanidmClient {
request: R,
) -> Result<T, ClientError> {
let dest = format!("{}{}", self.get_url(), dest);
trace!("perform_auth_post_request connecting to {}", dest);
let req_string = serde_json::to_string(&request).map_err(ClientError::JsonEncode)?;
let response = self
@ -765,6 +780,7 @@ impl KanidmClient {
.map_err(|e| ClientError::JsonDecode(e, opid))
}
#[instrument(level = "debug", skip(self))]
async fn perform_get_request<T: DeserializeOwned>(&self, dest: &str) -> Result<T, ClientError> {
let dest = format!("{}{}", self.get_url(), dest);
let response = self.client.get(dest.as_str());
@ -906,6 +922,7 @@ impl KanidmClient {
.map_err(|e| ClientError::JsonDecode(e, opid))
}
#[instrument(level = "debug")]
pub async fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
let auth_init = AuthRequest {
step: AuthStep::Init2 {
@ -1068,11 +1085,13 @@ impl KanidmClient {
}
}
#[instrument(level = "debug")]
pub async fn auth_simple_password(
&self,
ident: &str,
password: &str,
) -> Result<(), ClientError> {
trace!("Init auth step");
let mechs = match self.auth_step_init(ident).await {
Ok(s) => s,
Err(e) => return Err(e),
@ -1508,6 +1527,7 @@ impl KanidmClient {
}
// == new credential update session code.
#[instrument(level = "debug", skip(self))]
pub async fn idm_person_account_credential_update_intent(
&self,
id: &str,
@ -1776,6 +1796,7 @@ impl KanidmClient {
}
// ==== Oauth2 resource server configuration
#[instrument(level = "debug")]
pub async fn idm_oauth2_rs_list(&self) -> Result<Vec<Entry>, ClientError> {
self.perform_get_request("/v1/oauth2").await
}

View file

@ -35,6 +35,20 @@ pub fn readonly(meta: &Metadata) -> bool {
)
}
#[cfg(target_family = "unix")]
#[test]
fn test_readonly() {
// check if the file Cargo.toml exists
use std::path::Path;
if Path::new("Cargo.toml").exists() == false {
panic!("Can't find Cargo.toml");
}
let meta = std::fs::metadata("Cargo.toml").expect("Can't find Cargo.toml");
println!("meta={:?} -> readonly={:?}", meta, readonly(&meta));
assert!(readonly(&meta) == false);
}
#[cfg(not(target_family = "unix"))]
/// Check a given file's metadata is read-only for the current user (true = read-only) Stub function if you're building for windows!
pub fn readonly(meta: &Metadata) -> bool {

View file

@ -14,7 +14,6 @@ repository = { workspace = true }
[dependencies]
async-trait = { workspace = true }
num_enum = { workspace = true }
tide = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-forest = { workspace = true, features = ["uuid", "smallvec", "tokio", "env-filter"] }

View file

@ -6,7 +6,6 @@ use tracing_forest::util::*;
use tracing_forest::Tag;
pub mod macros;
pub mod middleware;
pub use {tracing, tracing_forest, tracing_subscriber};

View file

@ -1,107 +0,0 @@
use tide::{self, Middleware, Next, Request};
use tracing::{self, instrument};
use crate::{request_error, request_info, request_warn, security_info, *};
pub struct TreeMiddleware {
trust_x_forward_for: bool,
}
impl TreeMiddleware {
pub fn new(trust_x_forward_for: bool) -> Self {
TreeMiddleware {
trust_x_forward_for,
}
}
#[instrument(name = "tide-request", skip(self, req, next))]
async fn log<'a, State: Clone + Send + Sync + 'static>(
&'a self,
mut req: Request<State>,
next: Next<'a, State>,
) -> tide::Result {
struct TreeMiddlewareFinished;
if req.ext::<TreeMiddlewareFinished>().is_some() {
return Ok(next.run(req).await);
}
req.set_ext(TreeMiddlewareFinished);
let remote_address = if self.trust_x_forward_for {
req.remote()
} else {
req.peer_addr()
}
.unwrap_or("-")
.to_string();
let host = req.host().unwrap_or("-").to_string();
let method = req.method();
let path = req.url().path().to_string();
let remote_address = remote_address.as_str();
let host = host.as_str();
let method = method.as_ref();
let path = path.as_str();
security_info!(
remote_addr = remote_address,
http.host = host,
http.method = method,
path,
"Request received"
);
let response = next.run(req).await;
let status = response.status();
if status.is_server_error() {
if let Some(error) = response.error() {
request_error!(
message = display(error),
error_type = error.type_name().unwrap_or("?"),
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"Internal error -> Response sent"
);
} else {
request_error!(
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"Internal error -> Response sent"
);
}
} else if status.is_client_error() {
if let Some(error) = response.error() {
request_warn!(
message = display(error),
error_type = error.type_name().unwrap_or("?"),
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"Client error --> Response sent"
);
} else if status == 404 {
// because not-found isn't really an error, is it?
request_info!(
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"--> Response sent"
);
} else {
request_warn!(
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"Client error --> Response sent"
);
}
} else {
request_info!(
status = format_args!("{} - {}", status as u16, status.canonical_reason()),
"--> Response sent"
);
}
Ok(response)
}
}
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> Middleware<State> for TreeMiddleware {
async fn handle(&self, req: Request<State>, next: Next<'_, State>) -> tide::Result {
self.log(req, next).await
}
}

View file

@ -96,7 +96,7 @@ echo "Adding ${TEST_USER_NAME} to ${TEST_GROUP}"
${KANIDM} group add-members "${TEST_GROUP}" "${TEST_USER_NAME}" -D idm_admin
echo "Enable experimental UI for admin idm_admin ${TEST_USER_NAME}"
${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}"
${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}" -D idm_admin
# create oauth2 rp
echo "Creating the OAuth2 RP"
@ -108,6 +108,8 @@ echo "Creating the OAuth2 RP Supplemental Scope Map"
${KANIDM} system oauth2 update-sup-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" admin -D admin
echo "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing."
echo "Checking the OAuth2 RP Exists"
${KANIDM} system oauth2 list -D admin | rg -A10 "${OAUTH2_RP_ID}"
# config auth2
echo "Pulling secret for the OAuth2 RP"

64
scripts/test_coverage.sh Executable file
View file

@ -0,0 +1,64 @@
#!/bin/bash
set -e
if [ "$(rustup default | grep -cE '^nightly' )" -eq 0 ]; then
echo "You need to switch to rust nightly!"
exit 1
fi
# if [ "$(which rustfilt | wc -l )" -eq 0 ]; then
# echo "You need to have rustfilt on the path"
# echo "cargo install rustfilt"
# exit 1
# fi
if [ "$(which llvm-cov | wc -l )" -eq 0 ]; then
echo "You need to have llvm-cov on the path"
exit 1
fi
export CARGO_INCREMENTAL=0
export LLVM_PROFILE_FILE
echo "Profile files going into ${LLVM_PROFILE_FILE}"
echo "Running tests"
#shellcheck disable=SC2068
LLVM_PROFILE_FILE="$(pwd)/target/profile/coverage-%p-%m.profraw" RUSTFLAGS="-C instrument-coverage" cargo test
grcov . --binary-path ./target/debug/deps/ \
-s . \
-t html \
--branch \
--ignore-not-existing \
--ignore '../*' \
--ignore "/*" \
-o target/coverage/html
# PROFDATA="./target/profile/kanidm.profdata"
# llvm-profdata merge ./target/profile/*.profraw -o "${PROFDATA}"
# llvm-cov report --ignore-filename-regex="\.cargo" \
# --enable-name-compression \
# $( \
# for file in \
# $( \
# RUSTFLAGS="-C instrument-coverage" \
# cargo test --tests --no-run --message-format=json \
# | jq -r "select(.profile.test == true) | .filenames[]" \
# | grep -v dSYM - \
# ); \
# do \
# printf "%s %s " -object $file; \
# done \
# ) \
# --instr-profile="${PROFDATA}" --summary-only
# llvm-cov show -Xdemangler=rustfilt target/debug/kanidmd \
# -instr-profile="${PROFDATA}" \
# -show-line-counts-or-regions \
# -show-instantiations \
# -name-regex="kani.*"

View file

@ -14,11 +14,20 @@ repository = { workspace = true }
[dependencies]
async-trait = { workspace = true }
axum = { workspace=true }
axum-auth = "0.4.0"
axum-csp = { workspace = true }
axum-macros = "0.3.7"
axum-server = { version = "0.5.1", features = ["tls-openssl"] }
axum-sessions = "0.5.0"
chrono = { workspace = true }
cron = { workspace = true }
compact_jwt = { workspace = true }
cron = { workspace = true }
futures-util = { workspace = true }
http = "0.2.9"
http-types = { workspace = true }
hyper = { workspace = true }
hyper-tls = { workspace = true }
kanidm_proto = { workspace = true }
kanidmd_lib = { workspace = true }
ldap3_proto = { workspace = true }
@ -26,18 +35,19 @@ libc = { workspace = true }
openssl = { workspace = true }
rand = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sketching = { workspace = true }
tide = { workspace = true }
time = { workspace = true, features = ["serde", "std","local-offset"] }
tide-compress = { workspace = true }
tide-openssl = { workspace = true }
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
tokio-openssl = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
toml = {workspace = true}
tower = { version = "0.4.13", features = ["tokio-stream", "tracing"] }
tower-http = { version = "0.4.1", features = ["tokio", "tracing", "uuid", "compression-gzip", "compression-zstd", "trace", "fs"] }
tracing = { workspace = true, features = ["attributes"] }
tracing-subscriber = { workspace = true, features = ["time", "json"] }
urlencoding = { workspace = true }
uuid = { workspace = true, features = ["serde", "v4" ] }

View file

@ -212,6 +212,7 @@ impl QueryServerReadV1 {
time::OffsetDateTime::now_utc()
}
};
#[allow(clippy::unwrap_used)]
let timestamp = now.format(&Rfc3339).unwrap();
let dest_file = format!("{}/backup-{}.json", outpath, timestamp);

View file

@ -290,16 +290,12 @@ impl QueryServerWriteV1 {
e
})?;
let mdf = ModifyEvent::from_internal_parts(
ident,
&modlist,
&filter,
&mut idms_prox_write.qs_write,
)
.map_err(|e| {
admin_error!(err = ?e, "Failed to begin modify");
e
})?;
let mdf =
ModifyEvent::from_internal_parts(ident, &modlist, &filter, &idms_prox_write.qs_write)
.map_err(|e| {
admin_error!(err = ?e, "Failed to begin modify");
e
})?;
trace!(?mdf, "Begin modify event");
@ -644,7 +640,7 @@ impl QueryServerWriteV1 {
#[instrument(
level = "info",
skip_all,
fields(uuid = ?eventid)
fields(uuid = ?eventid),
)]
pub async fn handle_idmcredentialupdateintent(
&self,

View file

@ -15,13 +15,13 @@ use kanidm_proto::messages::ConsoleOutputMode;
use serde::{Deserialize, Serialize};
use sketching::tracing_subscriber::EnvFilter;
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct IntegrationTestConfig {
pub admin_user: String,
pub admin_password: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OnlineBackup {
pub path: String,
#[serde(default = "default_online_backup_schedule")]
@ -38,7 +38,7 @@ fn default_online_backup_versions() -> usize {
7
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TlsConfiguration {
pub chain: String,
pub key: String,
@ -64,16 +64,22 @@ pub struct ServerConfig {
}
impl ServerConfig {
pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, ()> {
pub fn new<P: AsRef<Path>>(config_path: P) -> Result<Self, std::io::Error> {
let mut f = File::open(config_path).map_err(|e| {
eprintln!("Unable to open config file [{:?}] 🥺", e);
e
})?;
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| eprintln!("unable to read contents {:?}", e))?;
f.read_to_string(&mut contents).map_err(|e| {
eprintln!("unable to read contents {:?}", e);
e
})?;
toml::from_str(contents.as_str()).map_err(|e| eprintln!("unable to parse config {:?}", e))
toml::from_str(contents.as_str()).map_err(|e| {
eprintln!("unable to parse config {:?}", e);
std::io::Error::new(std::io::ErrorKind::Other, e)
})
}
}
@ -142,9 +148,9 @@ impl ToString for LogLevel {
}
}
impl Into<EnvFilter> for LogLevel {
fn into(self) -> EnvFilter {
match self {
impl From<LogLevel> for EnvFilter {
fn from(value: LogLevel) -> Self {
match value {
LogLevel::Info => EnvFilter::new("info"),
LogLevel::Debug => EnvFilter::new("debug"),
LogLevel::Trace => EnvFilter::new("trace"),
@ -152,7 +158,7 @@ impl Into<EnvFilter> for LogLevel {
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Configuration {
pub address: String,
pub ldapaddress: Option<String>,

View file

@ -0,0 +1,33 @@
use axum::extract::State;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use http::header::CONTENT_TYPE;
use kanidmd_lib::status::StatusRequestEvent;
use super::middleware::KOpId;
use super::ServerState;
/// Status endpoint used for healthchecks
pub async fn status(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let r = state
.status_ref
.handle_request(StatusRequestEvent {
eventid: kopid.eventid,
})
.await;
Response::new(format!("{}", r))
}
pub async fn robots_txt() -> impl IntoResponse {
(
[(CONTENT_TYPE, "text/plain;charset=utf-8")],
axum::response::Html(
r#"User-agent: *
Disallow: /
"#,
),
)
}

View file

@ -0,0 +1,57 @@
use std::path::PathBuf;
/// Generates the integrity hash for a file based on a filename
pub fn generate_integrity_hash(filename: String) -> Result<String, String> {
let wasm_filepath = PathBuf::from(filename);
match wasm_filepath.exists() {
false => Err(format!(
"Can't find {:?} to generate file hash",
&wasm_filepath
)),
true => {
let filecontents = match std::fs::read(&wasm_filepath) {
Ok(value) => value,
Err(error) => {
return Err(format!(
"Failed to read {:?}, skipping: {:?}",
wasm_filepath, error
));
}
};
let shasum = openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents)
.map_err(|_| {
format!(
"Failed to generate SHA384 hash for WASM at {:?}",
wasm_filepath
)
})?;
Ok(openssl::base64::encode_block(&shasum))
}
}
}
#[derive(Clone)]
pub struct JavaScriptFile {
// Relative to the pkg/ dir
pub filepath: &'static str,
// SHA384 hash of the file
pub hash: String,
// if it's a module add the "type"
pub filetype: Option<String>,
}
impl JavaScriptFile {
/// returns a `<script>` HTML tag
pub fn as_tag(&self) -> String {
let typeattr = match &self.filetype {
Some(val) => {
format!(" type=\"{}\"", val.as_str())
}
_ => String::from(""),
};
format!(
r#"<script src="/pkg/{}" integrity="sha384-{}"{}></script>"#,
self.filepath, &self.hash, &typeattr,
)
}
}

View file

@ -1,23 +1,29 @@
//! Builds a Progressive Web App Manifest page.
use axum::extract::State;
use axum::response::{IntoResponse, Response};
use axum::Extension;
use http::HeaderValue;
use serde::{Deserialize, Serialize};
///! Builds a Progressive Web App Manifest page.
use super::middleware::KOpId;
// Thanks to the webmanifest crate for a lot of this code
use crate::https::{AppState, RequestExtensions};
use super::ServerState;
/// The MIME type for `.webmanifest` files.
const MIME_TYPE_MANIFEST: &str = "application/manifest+json;charset=utf-8";
/// Create a new manifest builder.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest<'s> {
name: &'s str,
short_name: &'s str,
start_url: &'s str,
pub struct Manifest {
name: String,
short_name: String,
start_url: String,
#[serde(rename = "display")]
display_mode: DisplayMode,
background_color: &'s str,
background_color: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<&'s str>,
description: Option<String>,
#[serde(rename = "dir")]
direction: Direction,
// direction: Option<Direction>,
@ -25,11 +31,11 @@ pub struct Manifest<'s> {
orientation: Option<String>,
// orientation: Option<Orientation>,
#[serde(skip_serializing_if = "Option::is_none")]
lang: Option<&'s str>,
lang: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<&'s str>,
scope: Option<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
theme_color: &'s str,
theme_color: String,
#[serde(skip_serializing_if = "Option::is_none")]
prefer_related_applications: Option<bool>,
// #[serde(borrow)]
@ -94,12 +100,7 @@ enum DisplayMode {
Browser,
}
/// Generates a manifest.json file for progressive web app usage
pub async fn manifest(req: tide::Request<AppState>) -> tide::Result {
let mut res = tide::Response::new(200);
let (eventid, _) = req.new_eventid();
let domain_display_name = req.state().qe_r_ref.get_domain_display_name(eventid).await;
pub fn manifest_data(host_req: Option<&str>, domain_display_name: String) -> Manifest {
let icons = vec![
ManifestIcon {
sizes: String::from("512x512"),
@ -127,32 +128,45 @@ pub async fn manifest(req: tide::Request<AppState>) -> tide::Result {
},
];
let start_url = match req.host() {
let start_url = match host_req {
Some(value) => format!("https://{}/", value),
None => String::from("/"),
};
let manifest_struct = Manifest {
short_name: "Kanidm",
name: domain_display_name.as_str(),
start_url: start_url.as_str(),
Manifest {
short_name: "Kanidm".to_string(),
name: domain_display_name,
start_url,
display_mode: DisplayMode::MinimalUi,
description: None,
orientation: None,
lang: Some("en"),
theme_color: "white",
background_color: "white",
lang: Some("en".to_string()),
theme_color: "white".to_string(),
background_color: "white".to_string(),
direction: Direction::Auto,
scope: None,
prefer_related_applications: None,
icons,
related_applications: None,
};
let manifest_string = serde_json::to_string_pretty(&manifest_struct)?;
res.set_content_type(MIME_TYPE_MANIFEST);
res.set_body(manifest_string);
Ok(res)
}
}
/// Generates a manifest.json file for progressive web app usage
pub(crate) async fn manifest(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await;
// TODO: fix the None here to make it the request host
let manifest_string =
match serde_json::to_string_pretty(&manifest_data(None, domain_display_name)) {
Ok(val) => val,
Err(_) => String::from(""),
};
let mut res = Response::new(manifest_string);
res.headers_mut()
.insert("Content-Type", HeaderValue::from_static(MIME_TYPE_MANIFEST));
res
}

View file

@ -1,229 +0,0 @@
use regex::Regex;
///! Custom tide middleware for Kanidm
use crate::https::JavaScriptFile;
/// This is for the tide_compression middleware so that we only compress certain content types.
///
/// ```
/// use kanidmd_core::https::middleware::compression_content_type_checker;
/// let these_should_match = vec![
/// "application/wasm",
/// "application/x-javascript",
/// "application/x-javascript; charset=utf-8",
/// "image/svg+xml",
/// "text/json",
/// "text/javascript",
/// ];
/// for test_value in these_should_match {
/// eprintln!("checking {:?}", test_value);
/// assert!(compression_content_type_checker().is_match(test_value));
/// }
/// assert!(compression_content_type_checker().is_match("application/wasm"));
/// let these_should_be_skipped = vec![
/// "application/manifest+json",
/// "image/jpeg",
/// "image/wasm",
/// "text/html",
/// ];
/// for test_value in these_should_be_skipped {
/// eprintln!("checking {:?}", test_value);
/// assert!(!compression_content_type_checker().is_match(test_value));
/// }
/// ```
pub fn compression_content_type_checker() -> Regex {
Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|x-javascript|xml|wasm))(|; charset=utf-8)$")
.expect("regex matcher for tide_compress content-type check failed to compile")
}
#[derive(Default)]
pub struct CacheableMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for CacheableMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=300,must-revalidate,private");
Ok(response)
}
}
#[derive(Default)]
pub struct NoCacheMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for NoCacheMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "no-store, max-age=0");
response.insert_header("Pragma", "no-cache");
Ok(response)
}
}
#[derive(Default)]
/// Sets Cache-Control headers on static content endpoints
pub struct StaticContentMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StaticContentMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("Cache-Control", "max-age=3600,private");
Ok(response)
}
}
#[derive(Default)]
/// Adds the following headers to responses
/// - x-frame-options
/// - x-content-type-options
/// - cross-origin-resource-policy
/// - cross-origin-embedder-policy
/// - cross-origin-opener-policy
pub struct StrictResponseMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictResponseMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("cross-origin-embedder-policy", "require-corp");
response.insert_header("cross-origin-opener-policy", "same-origin");
response.insert_header("cross-origin-resource-policy", "same-origin");
response.insert_header("x-content-type-options", "nosniff");
Ok(response)
}
}
#[derive(Default)]
struct StrictRequestMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for StrictRequestMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let proceed = request
.header("sec-fetch-site")
.map(|hv| {
matches!(hv.as_str(), "same-origin" | "same-site" | "none")
|| (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate")
&& request.method() == tide::http::Method::Get
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object")
&& request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed"))
})
.unwrap_or(true);
if proceed {
Ok(next.run(request).await)
} else {
Err(tide::Error::from_str(
tide::StatusCode::MethodNotAllowed,
"StrictRequestViolation",
))
}
}
}
#[derive(Default)]
/// This tide MiddleWare adds headers like Content-Security-Policy
/// and similar families. If it keeps adding more things then
/// probably rename the middleware :)
pub struct UIContentSecurityPolicyResponseMiddleware {
// The sha384 hash of /pkg/wasmloader.js
pub hashes: Vec<JavaScriptFile>,
}
impl UIContentSecurityPolicyResponseMiddleware {
pub fn new(hashes: Vec<JavaScriptFile>) -> Self {
Self { hashes }
}
}
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State>
for UIContentSecurityPolicyResponseMiddleware
{
// This updates the UI body with the integrity hash value for the wasmloader.js file, and adds content-security-policy headers.
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
// a list of hashes of js files that we're sending to the user
let hashes: Vec<String> = self
.hashes
.iter()
.map(|j| format!("'{}'", j.hash))
.collect();
response.insert_header(
/* content-security-policy headers tell the browser what to trust
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
In this case we're only trusting the same server that the page is
loaded from, and adding a hash of wasmloader.js, which is the main script
we should be loading, and should be really secure about that!
*/
"content-security-policy",
vec![
"default-src 'self'",
// TODO: #912 have a dev/test mode where we can rebuild the hashes on page load, so when doing constant JS changes/rebuilds we don't have to restart the server every time. It'd be *terrible* to run in prod because of the constant disk thrashing, but nicer for devs.
// we need unsafe-eval because of WASM things
format!("script-src 'self' {} 'unsafe-eval'", hashes.join(" ")).as_str(),
"form-action https: 'self'", // to allow for OAuth posts
// we are not currently using workers so it can be blocked
"worker-src 'none'",
// TODO: Content-Security-Policy-Report-Only https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
// "report-to 'none'", // unsupported by a lot of things still, but mozilla's saying report-uri is deprecated?
// Commented because when violated this attempts to post to "'none'" as a url
// "report-uri 'none'",
"base-uri 'self'",
// nobody wants to be in a frame
"frame-ancestors 'none'",
// allow inline images because bootstrap
"img-src 'self' data:",
]
.join(";"),
);
Ok(response)
}
}
const KANIDM_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Default)]
pub struct VersionHeaderMiddleware;
#[async_trait::async_trait]
impl<State: Clone + Send + Sync + 'static> tide::Middleware<State> for VersionHeaderMiddleware {
async fn handle(
&self,
request: tide::Request<State>,
next: tide::Next<'_, State>,
) -> tide::Result {
let mut response = next.run(request).await;
response.insert_header("X-KANIDM-VERSION", KANIDM_VERSION);
Ok(response)
}
}

View file

@ -0,0 +1,20 @@
use axum::{
http::{self, Request},
middleware::Next,
response::Response,
};
/// Adds `no-cache max-age=0` to the response headers.
pub async fn dont_cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
let mut response = next.run(request).await;
response.headers_mut().insert(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("no-store no-cache max-age=0"),
);
response.headers_mut().insert(
http::header::PRAGMA,
http::HeaderValue::from_static("no-cache"),
);
response
}

View file

@ -0,0 +1,25 @@
/*
Let's build a compression middleware!
The threat of the TLS BREACH attack [1] was considered as part of adding
the CompressMiddleware configuration.
The attack targets secrets compressed and encrypted in flight with the intent
to infer their content.
This is not a concern for the paths covered by this configuration
( /, /ui/<and all sub-paths>, /pkg/<and all sub-paths> ),
as they're all static content with no secrets in transit - all that data should
come from Kanidm's REST API, which is on a different path and not covered by
the compression middleware.
[1] - https://resources.infosecinstitute.com/topic/the-breach-attack/
*/
use tower_http::compression::CompressionLayer;
pub fn new() -> CompressionLayer {
CompressionLayer::new()
.no_br()
.quality(tower_http::CompressionLevel::Best)
}

View file

@ -0,0 +1,21 @@
use axum::extract::State;
use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use crate::https::ServerState;
pub async fn cspheaders_layer<B>(
State(state): State<ServerState>,
request: Request<B>,
next: Next<B>,
) -> Response {
// wait for the middleware to come back
let mut response = next.run(request).await;
// add the header
let headers = response.headers_mut();
headers.insert("Content-Security-Policy", state.csp_header);
response
}

View file

@ -0,0 +1,89 @@
use axum::{
headers::{authorization::Bearer, Authorization},
http::{self, Request},
middleware::Next,
response::Response,
Extension, TypedHeader,
};
use axum_sessions::SessionHandle;
use http::HeaderValue;
use uuid::Uuid;
pub(crate) mod caching;
pub(crate) mod compression;
pub(crate) mod csp_headers;
// the version middleware injects
const KANIDM_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Injects a header into the response with "X-KANIDM-VERSION" matching the version of the package.
pub async fn version_middleware<B>(request: Request<B>, next: Next<B>) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
headers.insert("X-KANIDM-VERSION", HeaderValue::from_static(KANIDM_VERSION));
response
}
#[derive(Clone, Debug)]
/// For holding onto the event ID and other handy request-based things
pub struct KOpId {
pub eventid: Uuid,
pub uat: Option<String>,
}
impl KOpId {
/// Return the event ID as a string
pub fn eventid_value(&self) -> String {
let res = self.eventid;
res.as_hyphenated().to_string()
}
}
/// This runs at the start of the request, adding an extension with `KOpId` which has useful things inside it.
pub async fn kopid_start<B>(
auth: Option<TypedHeader<Authorization<Bearer>>>,
mut request: Request<B>,
next: Next<B>,
) -> Response {
// generate the event ID
let eventid = sketching::tracing_forest::id();
// get the bearer token from the headers or the session
let uat = match auth {
Some(bearer) => Some(bearer.token().to_string()),
None => {
// no headers, let's try the cookies
match request.extensions().get::<SessionHandle>() {
Some(sess) => {
// we have a session!
sess.read().await.get::<String>("bearer")
}
None => None,
}
}
};
// insert the extension so we can pull it out later
request.extensions_mut().insert(KOpId { eventid, uat });
next.run(request).await
}
/// This runs at the start of the request, adding an extension with the OperationID
pub async fn kopid_end<B>(
Extension(kopid): Extension<KOpId>,
request: Request<B>,
next: Next<B>,
) -> Response {
// generate the event ID
// insert the extension so we can pull it out later
let mut response = next.run(request).await;
#[allow(clippy::unwrap_used)]
response.headers_mut().insert(
"X-KANIDM-OPID",
HeaderValue::from_str(&kopid.eventid_value()).unwrap(),
);
response
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,94 +0,0 @@
///! Route-mapping magic for tide
///
/// Instead of adding routes with (for example) the .post method you add them with .mapped_post, passing an instance of [RouteMap] and it'll do the rest...
use serde::{Deserialize, Serialize};
use tide::{Endpoint, Route};
use crate::https::AppState;
// Extends the tide::Route for RouteMaps, this would really be nice if it was generic :(
pub trait RouteMaps {
fn mapped_method(
&mut self,
routemap: &mut RouteMap,
method: http_types::Method,
ep: impl Endpoint<AppState>,
) -> &mut Self;
fn mapped_delete(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
fn mapped_get(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
fn mapped_patch(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
fn mapped_post(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
fn mapped_put(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
fn mapped_update(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self;
}
impl RouteMaps for Route<'_, AppState> {
// add a mapped method to the list
fn mapped_method(
&mut self,
routemap: &mut RouteMap,
method: http_types::Method,
ep: impl Endpoint<AppState>,
) -> &mut Self {
// TODO: truly weird things involving ASTs and sacrifices to eldritch gods to figure out how to represent the Endpoint
// if the path is empty then it's the root path...
let path_str = self.path().to_string();
let path = match path_str.is_empty() {
true => String::from("/"),
false => path_str,
};
// debug!("Mapping route: {:?}", path);
routemap.routelist.push(RouteInfo { path, method });
self.method(method, ep)
}
fn mapped_delete(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Delete, ep)
}
fn mapped_get(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Get, ep)
}
fn mapped_patch(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Patch, ep)
}
fn mapped_post(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Post, ep)
}
fn mapped_put(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Put, ep)
}
fn mapped_update(&mut self, routemap: &mut RouteMap, ep: impl Endpoint<AppState>) -> &mut Self {
self.mapped_method(routemap, http_types::Method::Update, ep)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
/// Information about a given route
pub struct RouteInfo {
pub path: String,
pub method: http_types::Method,
}
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct RouteMap {
pub routelist: Vec<RouteInfo>,
}
impl RouteMap {
// Serializes the object out to a pretty JSON blob
pub fn do_map(&self) -> String {
serde_json::to_string_pretty(self).unwrap()
}
// Inject the route for the routemap endpoint
pub fn push_self(&mut self, path: String, method: http_types::Method) {
self.routelist.push(RouteInfo { path, method });
}
}

View file

@ -0,0 +1,23 @@
#[test]
fn test_javscriptfile() {
// make sure it outputs what we think it does
use crate::https::JavaScriptFile;
let jsf = JavaScriptFile {
filepath: "wasmloader.js",
hash: "1234567890".to_string(),
filetype: Some("module".to_string()),
};
assert_eq!(
jsf.as_tag(),
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890" type="module"></script>"#
);
let jsf = JavaScriptFile {
filepath: "wasmloader.js",
hash: "1234567890".to_string(),
filetype: None,
};
assert_eq!(
jsf.as_tag(),
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890"></script>"#
);
}

View file

@ -0,0 +1,65 @@
use axum::extract::State;
use axum::http::HeaderValue;
use axum::response::Response;
use axum::Extension;
use super::middleware::KOpId;
use super::ServerState;
pub async fn ui_handler(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> Response<String> {
let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await;
// this feels icky but I felt that adding a trait on Vec<JavaScriptFile> which generated the string was going a bit far
let jsfiles: Vec<String> = state.js_files.into_iter().map(|j| j.as_tag()).collect();
let jstags = jsfiles.join(" ");
let body = format!(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<title>{}</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/pkg/img/logo-180.png" />
<link rel="apple-touch-icon" sizes="192x192" href="/pkg/img/logo-192.png" />
<link rel="apple-touch-icon" sizes="512x512" href="/pkg/img/logo-square.svg" />
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
<link rel="stylesheet" href="/pkg/style.css"/>
{}
</head>
<body class="flex-column d-flex h-100">
<main class="flex-shrink-0 form-signin">
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
<h3>Kanidm is loading, please wait... </h3>
</center>
</main>
<footer class="footer mt-auto py-3 bg-light text-end">
<div class="container">
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span>
</div>
</footer>
</body>
</html>"#,
domain_display_name.as_str(),
jstags,
);
let mut res = Response::new(body);
res.headers_mut().insert(
"Content-Type",
HeaderValue::from_static("text/html;charset=utf-8"),
);
res
}

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,10 @@
use super::routemaps::{RouteMap, RouteMaps};
use super::{to_tide_response, AppState, RequestExtensions};
use super::middleware::KOpId;
use super::{to_axum_response, ServerState};
use axum::extract::{Path, State};
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use axum_auth::AuthBearer;
use kanidm_proto::scim_v1::ScimSyncRequest;
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidmd_lib::prelude::*;
@ -9,290 +14,246 @@ use super::v1::{
json_rest_event_put_id_attr,
};
pub async fn sync_account_get(req: tide::Request<AppState>) -> tide::Result {
pub async fn sync_account_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_get(req, filter, None).await
json_rest_event_get(state, None, filter, kopid).await
}
pub async fn sync_account_post(req: tide::Request<AppState>) -> tide::Result {
pub async fn sync_account_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
Json(obj): Json<ProtoEntry>,
) -> impl IntoResponse {
let classes = vec!["sync_account".to_string(), "object".to_string()];
json_rest_event_post(req, classes).await
json_rest_event_post(state, classes, obj, kopid).await
}
pub async fn sync_account_id_get(req: tide::Request<AppState>) -> tide::Result {
pub async fn sync_account_id_get(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_get_id(req, filter, None).await
json_rest_event_get_id(state, id, filter, None, kopid).await
}
pub async fn sync_account_id_get_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_get_id_attr(req, filter).await
}
pub async fn sync_account_id_put_attr(req: tide::Request<AppState>) -> tide::Result {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_put_id_attr(req, filter).await
}
pub async fn sync_account_id_patch(mut req: tide::Request<AppState>) -> tide::Result {
// Update a value / attrs
let uat = req.get_current_uat();
let id = req.get_url_param("id")?;
let obj: ProtoEntry = req.body_json().await?;
pub async fn sync_account_id_patch(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
Json(obj): Json<ProtoEntry>,
) -> impl IntoResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
let res = state
.qe_w_ref
.handle_internalpatch(uat, filter, obj, eventid)
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
pub async fn sync_account_id_get_finalise(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
pub async fn sync_account_id_get_finalise(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_sync_account_finalise(uat, uuid_or_name, eventid)
.handle_sync_account_finalise(kopid.uat, id, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
pub async fn sync_account_id_get_terminate(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
pub async fn sync_account_id_get_terminate(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_sync_account_terminate(uat, uuid_or_name, eventid)
.handle_sync_account_terminate(kopid.uat, id, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
pub async fn sync_account_token_post(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let label: String = req.body_json().await?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
pub async fn sync_account_token_post(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
Json(label): Json<String>,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_sync_account_token_generate(uat, uuid_or_name, label, eventid)
.handle_sync_account_token_generate(kopid.uat, id, label, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
pub async fn sync_account_token_delete(req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let uuid_or_name = req.get_url_param("id")?;
let (eventid, hvalue) = req.new_eventid();
let res = req
.state()
pub async fn sync_account_token_delete(
State(state): State<ServerState>,
Path(id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_sync_account_token_destroy(uat, uuid_or_name, eventid)
.handle_sync_account_token_destroy(kopid.uat, id, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
async fn scim_sync_post(mut req: tide::Request<AppState>) -> tide::Result {
let (eventid, hvalue) = req.new_eventid();
// Given the token, and a sync update, apply the changes if any
let bearer = req.get_auth_bearer();
// Change this type later.
let changes: ScimSyncRequest = req.body_json().await?;
let res = req
.state()
async fn scim_sync_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
AuthBearer(bearer): AuthBearer,
Json(changes): Json<ScimSyncRequest>,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_scim_sync_apply(bearer, changes, eventid)
.handle_scim_sync_apply(Some(bearer), changes, kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
async fn scim_sync_get(req: tide::Request<AppState>) -> tide::Result {
let (eventid, hvalue) = req.new_eventid();
async fn scim_sync_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
AuthBearer(bearer): AuthBearer,
) -> impl IntoResponse {
// Given the token, what is it's connected sync state?
let bearer = req.get_auth_bearer();
trace!(?bearer);
let res = req
.state()
let res = state
.qe_r_ref
.handle_scim_sync_status(bearer, eventid)
.handle_scim_sync_status(Some(bearer), kopid.eventid)
.await;
to_tide_response(res, hvalue)
to_axum_response(res)
}
async fn scim_sink_get(req: tide::Request<AppState>) -> tide::Result {
let (_, hvalue) = req.new_eventid();
let mut res = tide::Response::new(200);
res.insert_header("X-KANIDM-OPID", hvalue);
res.set_content_type("text/html;charset=utf-8");
res.set_body(
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<title>Sink!</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
</head>
<body>
<pre>
___
.' _ '.
/ /` `\ \
| | [__]
| | {{
| | }}
_ | | _ {{
___________<_>_| |_<_>}}________
.=======^=(___)=^={{====.
/ .----------------}}---. \
/ / {{ \ \
/ / }} \ \
( '=========================' )
'-----------------------------'
</pre>
</body>
</html>"#,
);
Ok(res)
pub async fn sync_account_id_get_attr(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
Path((id, attr)): Path<(String, String)>,
) -> impl IntoResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_get_id_attr(state, id, attr, filter, kopid).await
}
pub fn scim_route_setup(appserver: &mut tide::Route<'_, AppState>, routemap: &mut RouteMap) {
let mut scim_process = appserver.at("/scim/v1");
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
//
// HTTP SCIM Usage
// Method
// ------ --------------------------------------------------------------
// GET Retrieves one or more complete or partial resources.
//
// POST Depending on the endpoint, creates new resources, creates a
// search request, or MAY be used to bulk-modify resources.
//
// PUT Modifies a resource by replacing existing attributes with a
// specified set of replacement attributes (replace). PUT
// MUST NOT be used to create new resources.
//
// PATCH Modifies a resource with a set of client-specified changes
// (partial update).
//
// DELETE Deletes a resource.
//
// Resource Endpoint Operations Description
// -------- ---------------- ---------------------- --------------------
// User /Users GET (Section 3.4.1), Retrieve, add,
// POST (Section 3.3), modify Users.
// PUT (Section 3.5.1),
// PATCH (Section 3.5.2),
// DELETE (Section 3.6)
//
// Group /Groups GET (Section 3.4.1), Retrieve, add,
// POST (Section 3.3), modify Groups.
// PUT (Section 3.5.1),
// PATCH (Section 3.5.2),
// DELETE (Section 3.6)
//
// Self /Me GET, POST, PUT, PATCH, Alias for operations
// DELETE (Section 3.11) against a resource
// mapped to an
// authenticated
// subject (e.g.,
// User).
//
// Service /ServiceProvider GET (Section 4) Retrieve service
// provider Config provider's
// config. configuration.
//
// Resource /ResourceTypes GET (Section 4) Retrieve supported
// type resource types.
//
// Schema /Schemas GET (Section 4) Retrieve one or more
// supported schemas.
//
// Bulk /Bulk POST (Section 3.7) Bulk updates to one
// or more resources.
//
// Search [prefix]/.search POST (Section 3.4.3) Search from system
// root or within a
// resource endpoint
// for one or more
// resource types using
// POST.
// -- Kanidm Resources
//
// Sync /Sync GET Retrieve the current
// sync state associated
// with the authenticated
// session
//
// POST Send a sync update
//
scim_process
.at("/Sync")
.mapped_post(routemap, scim_sync_post)
.mapped_get(routemap, scim_sync_get);
scim_process.at("/Sink").mapped_get(routemap, scim_sink_get);
let mut sync_account_route = appserver.at("/v1/sync_account");
sync_account_route
.at("/")
.mapped_get(routemap, sync_account_get)
.mapped_post(routemap, sync_account_post);
sync_account_route
.at("/:id")
.mapped_get(routemap, sync_account_id_get)
.mapped_patch(routemap, sync_account_id_patch);
sync_account_route
.at("/:id/_attr/:attr")
.mapped_get(routemap, sync_account_id_get_attr)
.mapped_put(routemap, sync_account_id_put_attr);
sync_account_route
.at("/:id/_finalise")
.mapped_get(routemap, sync_account_id_get_finalise);
sync_account_route
.at("/:id/_terminate")
.mapped_get(routemap, sync_account_id_get_terminate);
sync_account_route
.at("/:id/_sync_token")
// .mapped_get(&mut routemap, sync_account_token_get)
.mapped_post(routemap, sync_account_token_post)
.mapped_delete(routemap, sync_account_token_delete);
pub async fn sync_account_id_put_attr(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
Path((id, attr)): Path<(String, String)>,
Json(values): Json<Vec<String>>,
) -> impl IntoResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("sync_account")));
json_rest_event_put_id_attr(state, id, attr, filter, values, kopid).await
}
async fn scim_sink_get() -> impl IntoResponse {
r#"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="theme-color" content="white" />
<meta name="viewport" content="width=device-width" />
<title>Sink!</title>
<link rel="icon" href="/pkg/img/favicon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
</head>
<body>
<pre>
___
.' _ '.
/ /` `\ \
| | [__]
| | {{
| | }}
_ | | _ {{
___________<_>_| |_<_>}}________
.=======^=(___)=^={{====.
/ .----------------}}---. \
/ / {{ \ \
/ / }} \ \
( '=========================' )
'-----------------------------'
</pre>
</body>
</html>"#
}
pub fn scim_route_setup() -> Router<ServerState> {
Router::new()
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
//
// HTTP SCIM Usage
// Method
// ------ --------------------------------------------------------------
// GET Retrieves one or more complete or partial resources.
//
// POST Depending on the endpoint, creates new resources, creates a
// search request, or MAY be used to bulk-modify resources.
//
// PUT Modifies a resource by replacing existing attributes with a
// specified set of replacement attributes (replace). PUT
// MUST NOT be used to create new resources.
//
// PATCH Modifies a resource with a set of client-specified changes
// (partial update).
//
// DELETE Deletes a resource.
//
// Resource Endpoint Operations Description
// -------- ---------------- ---------------------- --------------------
// User /Users GET (Section 3.4.1), Retrieve, add,
// POST (Section 3.3), modify Users.
// PUT (Section 3.5.1),
// PATCH (Section 3.5.2),
// DELETE (Section 3.6)
//
// Group /Groups GET (Section 3.4.1), Retrieve, add,
// POST (Section 3.3), modify Groups.
// PUT (Section 3.5.1),
// PATCH (Section 3.5.2),
// DELETE (Section 3.6)
//
// Self /Me GET, POST, PUT, PATCH, Alias for operations
// DELETE (Section 3.11) against a resource
// mapped to an
// authenticated
// subject (e.g.,
// User).
//
// Service /ServiceProvider GET (Section 4) Retrieve service
// provider Config provider's
// config. configuration.
//
// Resource /ResourceTypes GET (Section 4) Retrieve supported
// type resource types.
//
// Schema /Schemas GET (Section 4) Retrieve one or more
// supported schemas.
//
// Bulk /Bulk POST (Section 3.7) Bulk updates to one
// or more resources.
//
// Search [prefix]/.search POST (Section 3.4.3) Search from system
// root or within a
// resource endpoint
// for one or more
// resource types using
// POST.
// -- Kanidm Resources
//
// Sync /Sync GET Retrieve the current
// sync state associated
// with the authenticated
// session
//
// POST Send a sync update
//
.route("/v1/Sync", post(scim_sync_post).get(scim_sync_get))
.route("/v1/Sink", get(scim_sink_get))
}

View file

@ -28,7 +28,7 @@ extern crate kanidmd_lib;
pub mod actors;
pub mod config;
mod crypto;
pub mod https;
mod https;
mod interval;
mod ldaps;
@ -911,26 +911,27 @@ pub async fn create_server_core(
admin_info!("this config rocks! 🪨 ");
None
} else {
// ⚠️ only start the sockets and listeners in non-config-test modes.
let h = self::https::create_https_server(
config.address,
&config.domain,
config.tls_config.as_ref(),
config.role,
config.trust_x_forward_for,
&cookie_key,
let h: tokio::task::JoinHandle<()> = match https::create_https_server(
config.clone(),
cookie_key,
jws_signer,
status_ref,
server_write_ref,
server_read_ref,
broadcast_tx.subscribe(),
)
.await?;
.await
{
Ok(h) => h,
Err(e) => {
error!("Failed to start HTTPS server -> {:?}", e);
return Err(());
}
};
if config.role != ServerRole::WriteReplicaNoUI {
admin_info!("ready to rock! 🪨 UI available at: {}", config.origin);
admin_info!("ready to rock! 🪨 UI available at: {}", config.origin);
} else {
admin_info!("ready to rock! 🪨");
admin_info!("ready to rock! 🪨 ");
}
Some(h)
};

View file

@ -1,5 +1,7 @@
#!/bin/bash
set -e
# This script based on the developer readme and allows you to run a test server.
if [ -z "$KANI_CARGO_OPTS" ]; then

View file

@ -106,8 +106,8 @@ async fn main() -> ExitCode {
let mut config = Configuration::new();
// Check the permissions are OK.
let cfg_path = &opt.commands.commonopt().config_path; // TODO: this needs to be pulling from the default or something?
if format!("{}", cfg_path.display()) == "".to_string() {
config_error.push(format!("Refusing to run - config file path is empty"));
if cfg_path.display().to_string().is_empty() {
config_error.push("Refusing to run - config file path is empty".to_string());
}
if !cfg_path.exists() {
config_error.push(format!(
@ -149,7 +149,7 @@ async fn main() -> ExitCode {
// Fall back to stderr
.map_sender(|sender| sender.or_stderr())
.build_on(|subscriber|{
let sub = subscriber.with(log_filter);
subscriber.with(log_filter)
// this does NOT work, it just adds a layer.
// if std::io::stdout().is_terminal() {
// println!("Stdout is a terminal");
@ -158,7 +158,6 @@ async fn main() -> ExitCode {
// println!("Stdout is not a terminal");
// sub.with(sketching::tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
// }
sub
})
.on(async {
// Get information on the windows username
@ -200,7 +199,13 @@ async fn main() -> ExitCode {
return ExitCode::SUCCESS
};
let sconfig = sconfig.expect("Somehow you got an empty ServerConfig after error checking?");
let sconfig = match sconfig {
Some(val) => val,
None => {
error!("Somehow you got an empty ServerConfig after error checking?");
return ExitCode::FAILURE
}
};
#[cfg(target_family = "unix")]
@ -313,7 +318,7 @@ async fn main() -> ExitCode {
/*
// Apply any cli overrides, normally debug level.
if opt.commands.commonopt().debug.as_ref() {
// ::std::env::set_var("RUST_LOG", "tide=info,kanidm=info,webauthn=debug");
// ::std::env::set_var("RUST_LOG", ",kanidm=info,webauthn=debug");
}
*/
@ -383,30 +388,35 @@ async fn main() -> ExitCode {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
@ -554,17 +564,33 @@ async fn main() -> ExitCode {
let ca_cert_path = PathBuf::from(ca_cert);
match ca_cert_path.exists() {
true => {
let ca_contents = std::fs::read_to_string(ca_cert_path.clone()).expect(&format!("Failed to read {}!", ca_cert));
let ca_contents = match std::fs::read_to_string(ca_cert_path.clone()) {
Ok(val) => val,
Err(e) => {
error!("Failed to read {:?} from filesystem: {:?}", ca_cert_path, e);
return ExitCode::FAILURE
}
};
let content = ca_contents
.split("-----END CERTIFICATE-----")
.into_iter()
.filter_map(|c| if c.trim().is_empty() { None } else { Some(c.trim().to_string())})
.collect::<Vec<String>>();
let content = content.last().expect(&format!("Failed to pull the last chunk of {} as a valid certificate!", ca_cert));
let content = match content.last() {
Some(val) => val,
None => {
error!("Failed to parse {:?} as valid certificate", ca_cert_path);
return ExitCode::FAILURE
}
};
let content = format!("{}-----END CERTIFICATE-----", content);
let ca_cert_parsed = reqwest::Certificate::from_pem(content.as_bytes())
.expect(&format!("Failed to parse {} as a valid certificate!\n{}", ca_cert, content));
let ca_cert_parsed = match reqwest::Certificate::from_pem(content.as_bytes()) {
Ok(val) => val,
Err(e) =>{
error!("Failed to parse {} as a valid certificate!\nError: {:?}", ca_cert, e);
return ExitCode::FAILURE
}
};
client.add_root_certificate(ca_cert_parsed)
},
false => {
@ -574,7 +600,7 @@ async fn main() -> ExitCode {
}
}
};
#[allow(clippy::unwrap_used)]
let client = client
.build()
.unwrap();

View file

@ -51,7 +51,6 @@ sketching = { workspace = true }
smartstring = { workspace = true, features = ["serde"] }
smolset = { workspace = true }
sshkeys = { workspace = true }
tide = { workspace = true }
time = { workspace = true, features = ["serde", "std"] }
tokio = { workspace = true, features = ["net", "sync", "time", "rt"] }
tokio-util = { workspace = true, features = ["codec"] }

View file

@ -440,7 +440,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// At lease *one* must be modifiable OR visible.
if !(primary_can_edit || passkeys_can_edit || ext_cred_portal_can_view) {
error!("Unable to proceed with credential update intent - at least one type of credential must be modifiable or visible.");
return Err(OperationError::NotAuthorised);
Err(OperationError::NotAuthorised)
} else {
security_info!(%primary_can_edit, %passkeys_can_edit, %ext_cred_portal_can_view, "Proceeding");
Ok((
@ -459,7 +459,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
sessionid: Uuid,
intent_token_id: Option<String>,
account: Account,
perms: &CredUpdateSessionPerms,
perms: CredUpdateSessionPerms,
ct: Duration,
) -> Result<(CredentialUpdateSessionToken, CredentialUpdateSessionStatus), OperationError> {
let ext_cred_portal_can_view = perms.ext_cred_portal_can_view;
@ -809,7 +809,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
// ==========
// Okay, good to exchange.
self.create_credupdate_session(session_id, Some(intent_id), account, &perms, current_time)
self.create_credupdate_session(session_id, Some(intent_id), account, perms, current_time)
}
#[instrument(level = "debug", skip_all)]
@ -826,7 +826,7 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
let sessionid = uuid_from_duration(ct + MAXIMUM_CRED_UPDATE_TTL, self.sid);
// Build the cred update session.
self.create_credupdate_session(sessionid, None, account, &perms, ct)
self.create_credupdate_session(sessionid, None, account, perms, ct)
}
#[instrument(level = "trace", skip(self))]
@ -1141,6 +1141,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
Ok(status)
}
#[instrument(level = "debug", skip(self))]
fn check_password_quality(
&self,
cleartext: &str,
@ -1284,6 +1285,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
}
}
#[instrument(level = "trace", skip(cust, self))]
pub fn credential_primary_set_password(
&self,
cust: &CredentialUpdateSessionToken,

View file

@ -1796,6 +1796,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
}
}
// TODO: this can be handled by the auth header parsers in axum
fn parse_basic_authz(client_authz: &str) -> Result<(String, String), Oauth2Error> {
// Check the client_authz
let authz = general_purpose::STANDARD

View file

@ -63,7 +63,7 @@ impl NameHistory {
}
fn handle_name_creation(
cands: &mut Vec<EntryInvalidNew>,
cands: &mut [EntryInvalidNew],
cid: &Cid,
) -> Result<(), OperationError> {
for cand in cands.iter_mut() {

View file

@ -1923,7 +1923,7 @@ impl Schema {
}
}
#[cfg(any(test))]
#[cfg(test)]
pub(crate) fn write_blocking(&self) -> SchemaWriteTransaction<'_> {
self.write()
}

View file

@ -424,7 +424,7 @@ impl<'a> QueryServerWriteTransaction<'a> {
.into_iter()
.try_for_each(|entry| self.internal_migrate_or_create(entry));
if !r.is_ok() {
if r.is_err() {
error!(res = ?r, "initialise_schema_idm -> Error");
}
debug_assert!(r.is_ok());

View file

@ -23,7 +23,7 @@ kanidmd_lib = { workspace = true }
url = { workspace = true, features = ["serde"] }
reqwest = { workspace = true, default-features = false }
reqwest = { workspace = true, default-features = false, features=["cookies"] }
sketching = { workspace = true }
testkit-macros = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }

View file

@ -20,16 +20,16 @@ use tokio::task;
pub const ADMIN_TEST_USER: &str = "admin";
pub const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
pub const NOT_ADMIN_TEST_USERNAME: &str = "krab_test_user";
pub const NOT_ADMIN_TEST_PASSWORD: &str = "eicieY7ahchaoCh0eeTa";
pub static PORT_ALLOC: AtomicU16 = AtomicU16::new(18080);
pub use testkit_macros::test;
pub fn is_free_port(port: u16) -> bool {
// TODO: Refactor to use `Result::is_err` in a future PR
match TcpStream::connect(("0.0.0.0", port)) {
Ok(_) => false,
Err(_) => true,
}
TcpStream::connect(("0.0.0.0", port)).is_err()
}
// Test external behaviours of the service.
@ -46,9 +46,10 @@ pub async fn setup_async_test() -> (KanidmClient, CoreHandle) {
break possible_port;
}
counter += 1;
#[allow(clippy::panic)]
if counter >= 5 {
eprintln!("Unable to allocate port!");
assert!(false);
panic!();
}
};
@ -67,21 +68,25 @@ pub async fn setup_async_test() -> (KanidmClient, CoreHandle) {
config.role = ServerRole::WriteReplica;
config.domain = "localhost".to_string();
config.origin = addr.clone();
// config.log_level = Some(LogLevel::Verbose as u32);
// config.log_level = Some(LogLevel::FullTrace as u32);
config.threads = 1;
let core_handle = create_server_core(config, false)
.await
.expect("failed to start server core");
// We have to yield now to guarantee that the tide elements are setup.
let core_handle = match create_server_core(config, false).await {
Ok(val) => val,
#[allow(clippy::panic)]
Err(_) => panic!("failed to start server core"),
};
// We have to yield now to guarantee that the elements are setup.
task::yield_now().await;
let rsclient = KanidmClientBuilder::new()
#[allow(clippy::panic)]
let rsclient = match KanidmClientBuilder::new()
.address(addr.clone())
.no_proxy()
.build()
.expect("Failed to build client");
{
Ok(val) => val,
Err(_) => panic!("failed to build client"),
};
tracing::info!("Testkit server setup complete - {}", addr);

View file

@ -4,7 +4,7 @@ use std::collections::HashSet;
use kanidm_client::KanidmClient;
use kanidm_proto::v1::{Filter, Modify, ModifyList};
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER};
use kanidmd_testkit::{ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, NOT_ADMIN_TEST_PASSWORD};
static USER_READABLE_ATTRS: [&str; 9] = [
"name",
@ -195,13 +195,13 @@ async fn login_account(rsclient: &KanidmClient, id: &str) {
.unwrap();
rsclient
.idm_person_account_primary_credential_set_password(id, "eicieY7ahchaoCh0eeTa")
.idm_person_account_primary_credential_set_password(id, NOT_ADMIN_TEST_PASSWORD)
.await
.unwrap();
let _ = rsclient.logout();
let res = rsclient
.auth_simple_password(id, "eicieY7ahchaoCh0eeTa")
.auth_simple_password(id, NOT_ADMIN_TEST_PASSWORD)
.await;
// Setup privs
@ -209,7 +209,7 @@ async fn login_account(rsclient: &KanidmClient, id: &str) {
assert!(res.is_ok());
let res = rsclient
.reauth_simple_password("eicieY7ahchaoCh0eeTa")
.reauth_simple_password(NOT_ADMIN_TEST_PASSWORD)
.await;
println!("{} priv granted for", id);
assert!(res.is_ok());
@ -758,3 +758,124 @@ async fn test_self_write_mail_priv_people(rsclient: KanidmClient) {
login_account_via_admin(&rsclient, "nonperson").await;
test_write_attrs(&rsclient, "nonperson", &["mail"], false).await;
}
#[kanidmd_testkit::test]
async fn test_https_robots_txt(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let response = match reqwest::get(format!("{}/robots.txt", &addr)).await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 200);
eprintln!(
"csp headers: {:#?}",
response.headers().get("content-security-policy")
);
assert_ne!(response.headers().get("content-security-policy"), None);
eprintln!("{}", response.text().await.unwrap());
}
// TODO: #1787 when the routemap comes back
// #[kanidmd_testkit::test]
// async fn test_https_routemap(rsclient: KanidmClient) {
// // We need to do manual reqwests here.
// let addr = rsclient.get_url();
// let response = match reqwest::get(format!("{}/v1/routemap", &addr)).await {
// Ok(value) => value,
// Err(error) => {
// panic!("Failed to query {:?} : {:#?}", addr, error);
// }
// };
// eprintln!("response: {:#?}", response);
// assert_eq!(response.status(), 200);
// let body = response.text().await.unwrap();
// eprintln!("{}", body);
// assert!(body.contains("/scim/v1/Sync"));
// assert!(body.contains(r#""path": "/v1/routemap""#));
// }
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_raw_delete(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let post_body = serde_json::json!({"filter": "self"}).to_string();
let response = match client
.post(format!("{}/v1/raw/delete", &addr))
.header("Content-Type", "application/json")
.body(post_body)
.send()
.await
{
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 401);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_raw_logout(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let response = match client.get(format!("{}/v1/logout", &addr)).send().await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 401);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_status_endpoint(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let response = match client.get(format!("{}/status", &addr)).send().await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 200);
let body = response.text().await.unwrap();
eprintln!("{}", body);
assert!(body.contains("true") == true);
}

View file

@ -0,0 +1,22 @@
use kanidm_client::KanidmClient;
#[kanidmd_testkit::test]
async fn test_https_manifest(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
// here we test the /ui/ endpoint which should have the headers
let response = match reqwest::get(format!("{}/manifest.webmanifest", &addr)).await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 200);
eprintln!(
"csp headers: {:#?}",
response.headers().get("content-security-policy")
);
}

View file

@ -14,26 +14,25 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 200);
eprintln!(
"csp headers: {:#?}",
response.headers().get("content-security-policy")
);
assert_ne!(response.headers().get("content-security-policy"), None);
// here we test the /pkg/ endpoint which shouldn't have the headers
let response =
match reqwest::get(format!("{}/pkg/external/bootstrap.bundle.min.js", &addr)).await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
// here we test the /ui/login endpoint which should have the headers
let response = match reqwest::get(format!("{}/ui/login", &addr)).await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 200);
eprintln!(
"csp headers: {:#?}",
response.headers().get("content-security-policy")
);
assert_eq!(response.headers().get("content-security-policy"), None);
assert_ne!(response.headers().get("content-security-policy"), None);
}

View file

@ -9,6 +9,7 @@ use kanidm_proto::oauth2::{
AccessTokenResponse, AuthorisationResponse, GrantTypeReq, OidcDiscoveryResponse,
};
use oauth2_ext::PkceCodeChallenge;
use reqwest::StatusCode;
use url::Url;
use kanidm_client::KanidmClient;
@ -38,6 +39,11 @@ macro_rules! assert_no_cache {
}};
}
const TEST_INTEGRATION_RS_ID: &str = "test_integration";
const TEST_INTEGRATION_RS_GROUP_ALL: &str = "idm_all_accounts";
const TEST_INTEGRATION_RS_DISPLAY: &str = "Test Integration";
const TEST_INTEGRATION_RS_URL: &str = "https://demo.example.com";
#[kanidmd_testkit::test]
async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
let res = rsclient
@ -48,9 +54,9 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
// Create an oauth2 application integration.
rsclient
.idm_oauth2_rs_basic_create(
"test_integration",
"Test Integration",
"https://demo.example.com",
TEST_INTEGRATION_RS_ID,
TEST_INTEGRATION_RS_DISPLAY,
TEST_INTEGRATION_RS_URL,
)
.await
.expect("Failed to create oauth2 config");
@ -366,4 +372,110 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
eprintln!("oidc {oidc:?}");
assert!(userinfo == oidc);
// auth back with admin so we can test deleting things
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
rsclient
.idm_oauth2_rs_delete_sup_scope_map("test_integration", TEST_INTEGRATION_RS_GROUP_ALL)
.await
.expect("Failed to update oauth2 scopes");
}
#[kanidmd_testkit::test]
async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
let url = rsclient.get_url().to_string();
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
.expect("Failed to create client.");
// test for a bad-body request on token
let response = client
.post(format!("{}/oauth2/token", url))
.form(&serde_json::json!({}))
// .bearer_auth(atr.access_token.clone())
.send()
.await
.expect("Failed to send token request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY);
// test for a bad-auth request
let response = client
.post(format!("{}/oauth2/token/introspect", url))
.form(&serde_json::json!({ "token": "lol" }))
.send()
.await
.expect("Failed to send token introspection request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNAUTHORIZED);
}
#[kanidmd_testkit::test]
async fn test_oauth2_token_revoke_post(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
assert!(res.is_ok());
let url = rsclient.get_url().to_string();
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
.expect("Failed to create client.");
// test for a bad-body request on token
let response = client
.post(format!("{}/oauth2/token/revoke", url))
.form(&serde_json::json!({}))
.bearer_auth("lolol")
.send()
.await
.expect("Failed to send token request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY);
// test for a invalid format request on token
let response = client
.post(format!("{}/oauth2/token/revoke", url))
.json("")
.bearer_auth("lolol")
.send()
.await
.expect("Failed to send token request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE);
// test for a bad-body request on token
let response = client
.post(format!("{}/oauth2/token/revoke", url))
.form(&serde_json::json!({}))
.bearer_auth("Basic lolol")
.send()
.await
.expect("Failed to send token request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNPROCESSABLE_ENTITY);
// test for a bad-body request on token
let response = client
.post(format!("{}/oauth2/token/revoke", url))
.body(serde_json::json!({}).to_string())
.bearer_auth("Basic lolol")
.send()
.await
.expect("Failed to send token request.");
println!("{:?}", response);
assert!(response.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE);
}

View file

@ -0,0 +1,32 @@
use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_person_patch(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let post_body = serde_json::json!({"attrs": { "email" : "crab@example.com"}}).to_string();
let response = match client
.patch(format!("{}/v1/person/foo", &addr))
.header("Content-Type", "application/json")
.body(post_body)
.send()
.await
{
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 422);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}

View file

@ -0,0 +1,681 @@
use kanidm_client::KanidmClient;
#[kanidmd_testkit::test]
async fn test_routes(rsclient: KanidmClient) {
let routemap = r#"
[
{
"path": "/",
"method": "GET"
},
{
"path": "/robots.txt",
"method": "GET"
},
{
"path": "/manifest.webmanifest",
"method": "GET"
},
{
"path": "/ui/",
"method": "GET"
},
{
"path": "/ui/*",
"method": "GET"
},
{
"path": "/v1/account/:id/_unix/_token",
"method": "GET"
},
{
"path": "/v1/account/:id/_radius/_token",
"method": "GET"
},
{
"path": "/v1/group/:id/_unix/_token",
"method": "GET"
},
{
"path": "/v1/oauth2/:rs_name/_icon",
"method": "GET"
},
{
"path": "/status",
"method": "GET"
},
{
"path": "/oauth2/authorise",
"method": "POST"
},
{
"path": "/oauth2/authorise",
"method": "GET"
},
{
"path": "/oauth2/authorise/permit",
"method": "POST"
},
{
"path": "/oauth2/authorise/permit",
"method": "GET"
},
{
"path": "/oauth2/authorise/reject",
"method": "POST"
},
{
"path": "/oauth2/authorise/reject",
"method": "GET"
},
{
"path": "/oauth2/token",
"method": "POST"
},
{
"path": "/oauth2/token/introspect",
"method": "POST"
},
{
"path": "/oauth2/token/revoke",
"method": "POST"
},
{
"path": "/oauth2/openid/:client_id/.well-known/openid-configuration",
"method": "GET"
},
{
"path": "/oauth2/openid/:client_id/userinfo",
"method": "GET"
},
{
"path": "/oauth2/openid/:client_id/public_key.jwk",
"method": "GET"
},
{
"path": "/scim/v1/Sync",
"method": "POST"
},
{
"path": "/scim/v1/Sync",
"method": "GET"
},
{
"path": "/scim/v1/Sink",
"method": "GET"
},
{
"path": "/v1/sync_account",
"method": "GET"
},
{
"path": "/v1/sync_account",
"method": "POST"
},
{
"path": "/v1/sync_account/:id",
"method": "GET"
},
{
"path": "/v1/sync_account/:id",
"method": "PATCH"
},
{
"path": "/v1/sync_account/:id/_finalise",
"method": "GET"
},
{
"path": "/v1/sync_account/:id/_terminate",
"method": "GET"
},
{
"path": "/v1/sync_account/:id/_sync_token",
"method": "POST"
},
{
"path": "/v1/sync_account/:id/_sync_token",
"method": "DELETE"
},
{
"path": "/v1/raw/create",
"method": "POST"
},
{
"path": "/v1/raw/modify",
"method": "POST"
},
{
"path": "/v1/raw/delete",
"method": "POST"
},
{
"path": "/v1/raw/search",
"method": "POST"
},
{
"path": "/v1/auth",
"method": "POST"
},
{
"path": "/v1/auth/valid",
"method": "GET"
},
{
"path": "/v1/reauth",
"method": "POST"
},
{
"path": "/v1/logout",
"method": "GET"
},
{
"path": "/v1/schema",
"method": "GET"
},
{
"path": "/v1/schema/attributetype",
"method": "GET"
},
{
"path": "/v1/schema/attributetype",
"method": "POST"
},
{
"path": "/v1/schema/attributetype/:id",
"method": "GET"
},
{
"path": "/v1/schema/attributetype/:id",
"method": "PUT"
},
{
"path": "/v1/schema/attributetype/:id",
"method": "PATCH"
},
{
"path": "/v1/schema/classtype",
"method": "GET"
},
{
"path": "/v1/schema/classtype",
"method": "POST"
},
{
"path": "/v1/schema/classtype/:id",
"method": "GET"
},
{
"path": "/v1/schema/classtype/:id",
"method": "PUT"
},
{
"path": "/v1/schema/classtype/:id",
"method": "PATCH"
},
{
"path": "/v1/oauth2",
"method": "GET"
},
{
"path": "/v1/oauth2/_basic",
"method": "POST"
},
{
"path": "/v1/oauth2/:rs_name",
"method": "GET"
},
{
"path": "/v1/oauth2/:rs_name",
"method": "PATCH"
},
{
"path": "/v1/oauth2/:rs_name",
"method": "DELETE"
},
{
"path": "/v1/oauth2/:rs_name/_basic_secret",
"method": "GET"
},
{
"path": "/v1/oauth2/_scopemap/:id/:group",
"method": "POST"
},
{
"path": "/v1/oauth2/_scopemap/:id/:group",
"method": "DELETE"
},
{
"path": "/v1/oauth2/_sup_scopemap/:id/:group",
"method": "POST"
},
{
"path": "/v1/oauth2/_sup_scopemap/:id/:group",
"method": "DELETE"
},
{
"path": "/v1/self",
"method": "GET"
},
{
"path": "/v1/self/_uat",
"method": "GET"
},
{
"path": "/v1/self/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/self/_credential",
"method": "GET"
},
{
"path": "/v1/self/_credential/:cid/_lock",
"method": "GET"
},
{
"path": "/v1/self/_radius",
"method": "GET"
},
{
"path": "/v1/self/_radius",
"method": "DELETE"
},
{
"path": "/v1/self/_radius",
"method": "POST"
},
{
"path": "/v1/self/_radius/_config",
"method": "POST"
},
{
"path": "/v1/self/_radius/_config/:token",
"method": "GET"
},
{
"path": "/v1/self/_radius/_config/:token/apple",
"method": "GET"
},
{
"path": "/v1/self/_applinks",
"method": "GET"
},
{
"path": "/v1/person",
"method": "GET"
},
{
"path": "/v1/person",
"method": "POST"
},
{
"path": "/v1/person/:id",
"method": "GET"
},
{
"path": "/v1/person/:id",
"method": "PATCH"
},
{
"path": "/v1/person/:id",
"method": "DELETE"
},
{
"path": "/v1/person/:id/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/person/:id/_attr/:attr",
"method": "PUT"
},
{
"path": "/v1/person/:id/_attr/:attr",
"method": "POST"
},
{
"path": "/v1/person/:id/_attr/:attr",
"method": "DELETE"
},
{
"path": "/v1/person/:id/_lock",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential/_status",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential/:cid/_lock",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential/_update",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential/_update_intent",
"method": "GET"
},
{
"path": "/v1/person/:id/_credential/_update_intent/:ttl",
"method": "GET"
},
{
"path": "/v1/person/:id/_ssh_pubkeys",
"method": "GET"
},
{
"path": "/v1/person/:id/_ssh_pubkeys",
"method": "POST"
},
{
"path": "/v1/person/:id/_ssh_pubkeys/:tag",
"method": "GET"
},
{
"path": "/v1/person/:id/_ssh_pubkeys/:tag",
"method": "DELETE"
},
{
"path": "/v1/person/:id/_radius",
"method": "GET"
},
{
"path": "/v1/person/:id/_radius",
"method": "POST"
},
{
"path": "/v1/person/:id/_radius",
"method": "DELETE"
},
{
"path": "/v1/person/:id/_unix",
"method": "POST"
},
{
"path": "/v1/person/:id/_unix/_credential",
"method": "PUT"
},
{
"path": "/v1/person/:id/_unix/_credential",
"method": "DELETE"
},
{
"path": "/v1/service_account",
"method": "GET"
},
{
"path": "/v1/service_account",
"method": "POST"
},
{
"path": "/v1/service_account/:id",
"method": "GET"
},
{
"path": "/v1/service_account/:id",
"method": "PATCH"
},
{
"path": "/v1/service_account/:id",
"method": "DELETE"
},
{
"path": "/v1/service_account/:id/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_attr/:attr",
"method": "PUT"
},
{
"path": "/v1/service_account/:id/_attr/:attr",
"method": "POST"
},
{
"path": "/v1/service_account/:id/_attr/:attr",
"method": "DELETE"
},
{
"path": "/v1/service_account/:id/_lock",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_into_person",
"method": "POST"
},
{
"path": "/v1/service_account/:id/_api_token",
"method": "POST"
},
{
"path": "/v1/service_account/:id/_api_token",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_api_token/:token_id",
"method": "DELETE"
},
{
"path": "/v1/service_account/:id/_credential",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_credential/_generate",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_credential/_status",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_credential/:cid/_lock",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_ssh_pubkeys",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_ssh_pubkeys",
"method": "POST"
},
{
"path": "/v1/service_account/:id/_ssh_pubkeys/:tag",
"method": "GET"
},
{
"path": "/v1/service_account/:id/_ssh_pubkeys/:tag",
"method": "DELETE"
},
{
"path": "/v1/service_account/:id/_unix",
"method": "POST"
},
{
"path": "/v1/account/:id/_unix/_auth",
"method": "POST"
},
{
"path": "/v1/account/:id/_ssh_pubkeys",
"method": "GET"
},
{
"path": "/v1/account/:id/_ssh_pubkeys/:tag",
"method": "GET"
},
{
"path": "/v1/account/:id/_user_auth_token",
"method": "GET"
},
{
"path": "/v1/account/:id/_user_auth_token/:token_id",
"method": "DELETE"
},
{
"path": "/v1/credential/_exchange_intent",
"method": "POST"
},
{
"path": "/v1/credential/_status",
"method": "POST"
},
{
"path": "/v1/credential/_update",
"method": "POST"
},
{
"path": "/v1/credential/_commit",
"method": "POST"
},
{
"path": "/v1/credential/_cancel",
"method": "POST"
},
{
"path": "/v1/group",
"method": "GET"
},
{
"path": "/v1/group",
"method": "POST"
},
{
"path": "/v1/group/:id",
"method": "GET"
},
{
"path": "/v1/group/:id",
"method": "DELETE"
},
{
"path": "/v1/group/:id/_attr/:attr",
"method": "DELETE"
},
{
"path": "/v1/group/:id/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/group/:id/_attr/:attr",
"method": "PUT"
},
{
"path": "/v1/group/:id/_attr/:attr",
"method": "POST"
},
{
"path": "/v1/group/:id/_unix",
"method": "POST"
},
{
"path": "/v1/domain",
"method": "GET"
},
{
"path": "/v1/domain/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/domain/_attr/:attr",
"method": "PUT"
},
{
"path": "/v1/domain/_attr/:attr",
"method": "DELETE"
},
{
"path": "/v1/system",
"method": "GET"
},
{
"path": "/v1/system/_attr/:attr",
"method": "GET"
},
{
"path": "/v1/system/_attr/:attr",
"method": "POST"
},
{
"path": "/v1/system/_attr/:attr",
"method": "DELETE"
},
{
"path": "/v1/recycle_bin",
"method": "GET"
},
{
"path": "/v1/recycle_bin/:id",
"method": "GET"
},
{
"path": "/v1/recycle_bin/:id/_revive",
"method": "POST"
},
{
"path": "/v1/access_profile",
"method": "GET"
},
{
"path": "/v1/access_profile/:id",
"method": "GET"
},
{
"path": "/v1/access_profile/:id/_attr/:attr",
"method": "GET"
}
]
"#;
// ,{
// "path": "/v1/routemap",
// "method": "GET"
// }
let routelist: Vec<serde_json::Value> = serde_json::from_str(routemap).unwrap();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.unwrap();
for route in routelist {
// println!("{:?}", route);
let path: String = route.get("path").unwrap().to_string();
let method: String = route.get("method").unwrap().to_string();
let method = method.replace('"', "");
let method = method.as_str();
println!("'{method}'");
let method = match method {
"GET" => reqwest::Method::GET,
"POST" => reqwest::Method::POST,
"DELETE" => reqwest::Method::DELETE,
"PATCH" => reqwest::Method::PATCH,
"PUT" => reqwest::Method::PUT,
_ => todo!("{}", method),
};
if path.contains(':') {
println!("Can't do this because it has an attribute: {}", path);
continue;
}
let url = format!("{}{}", rsclient.get_url(), path.replace('"', ""));
println!("#### {:?} {} {}", method, path, url);
let res = match client
.request(method, &url)
// .version(http::Version::HTTP_11)
.send()
.await
{
Ok(val) => val,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", url, error);
}
};
if res.status() == 404 {
panic!("Failed to query {:?} : {:#?}", url, res);
}
}
}

View file

@ -2,6 +2,7 @@ use compact_jwt::JwsUnverified;
use kanidm_client::KanidmClient;
use kanidm_proto::internal::ScimSyncToken;
use kanidmd_testkit::ADMIN_TEST_PASSWORD;
use reqwest::header::HeaderValue;
use std::str::FromStr;
use url::Url;
@ -88,3 +89,37 @@ async fn test_sync_account_lifecycle(rsclient: KanidmClient) {
.await
.expect("Failed to destroy token");
}
#[kanidmd_testkit::test]
async fn test_scim_sync_get(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {:?}", rsclient.get_token().await)).unwrap(),
);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.default_headers(headers)
.build()
.unwrap();
// here we test the /ui/ endpoint which should have the headers
let response = match client.get(format!("{}/scim/v1/Sync", addr)).send().await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
// assert_eq!(response.status(), 200);
// eprintln!(
// "csp headers: {:#?}",
// response.headers().get("content-security-policy")
// );
// assert_ne!(response.headers().get("content-security-policy"), None);
// eprintln!("{}", response.text().await.unwrap());
}

View file

@ -0,0 +1,51 @@
use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_self_applinks(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let response = match client
.get(format!("{}/v1/self/_applinks", &addr))
.send()
.await
{
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 401);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_self_whoami_uat(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let response = match client.get(format!("{}/v1/self/_uat", &addr)).send().await {
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 401);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}

View file

@ -0,0 +1,30 @@
use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_service_account_id_attr_attr_delete(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
// let post_body = serde_json::json!({"filter": "self"}).to_string();
let response = match client
.delete(format!("{}/v1/service_account/admin/_attr/email", &addr))
.send()
.await
{
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 401);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}

View file

@ -0,0 +1,29 @@
use kanidm_client::KanidmClient;
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
#[kanidmd_testkit::test]
async fn test_v1_system_post_attr(rsclient: KanidmClient) {
// We need to do manual reqwests here.
let addr = rsclient.get_url();
let client = reqwest::ClientBuilder::new()
.danger_accept_invalid_certs(true)
.build()
.unwrap();
let response = match client
.post(format!("{}/v1/system/_attr/domain_name", &addr))
.json(&serde_json::json!({"filter": "self"}))
.send()
.await
{
Ok(value) => value,
Err(error) => {
panic!("Failed to query {:?} : {:#?}", addr, error);
}
};
eprintln!("response: {:#?}", response);
assert_eq!(response.status(), 422);
let body = response.text().await.unwrap();
eprintln!("{}", body);
}

View file

@ -188,30 +188,35 @@ async fn driver_main(opt: Opt) {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
@ -685,6 +690,7 @@ fn ipa_to_scim_entry(
debug!("{:#?}", sync_entry);
// check the sync_entry state?
#[allow(clippy::unimplemented)]
if sync_entry.state != LdapSyncStateValue::Add {
unimplemented!();
}
@ -732,7 +738,7 @@ fn ipa_to_scim_entry(
error!("Missing required attribute cn");
})?;
let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() {
let gidnumber = if let Some(number) = entry_config.map_gidnumber {
Some(number)
} else {
entry

View file

@ -178,30 +178,35 @@ async fn driver_main(opt: Opt) {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
@ -372,7 +377,7 @@ async fn run_sync(
}
};
let entries = match process_ldap_sync_result(entries, &sync_config).await {
let entries = match process_ldap_sync_result(entries, sync_config).await {
Ok(ssr) => ssr,
Err(()) => {
error!("Failed to process IPA entries to SCIM");
@ -454,6 +459,7 @@ fn ldap_to_scim_entry(
debug!("{:#?}", sync_entry);
// check the sync_entry state?
#[allow(clippy::unimplemented)]
if sync_entry.state != LdapSyncStateValue::Add {
unimplemented!();
}
@ -505,7 +511,7 @@ fn ldap_to_scim_entry(
);
})?;
let gidnumber = if let Some(number) = entry_config.map_gidnumber.clone() {
let gidnumber = if let Some(number) = entry_config.map_gidnumber {
Some(number)
} else {
entry

View file

@ -67,6 +67,8 @@ pub unsafe extern "C" fn cleanup<T>(_: *const PamHandle, c_data: *mut PamDataT,
pub type PamResult<T> = Result<T, PamResultCode>;
/// # Safety
///
/// Type-level mapping for safely retrieving values with `get_item`.
///
/// See `pam_get_item` in
@ -82,6 +84,8 @@ pub trait PamItem {
}
impl PamHandle {
/// # Safety
///
/// Gets some value, identified by `key`, that has been set by the module
/// previously.
///

View file

@ -376,13 +376,13 @@ async fn process_etc_passwd_group(cachelayer: &CacheLayer) -> Result<(), Box<dyn
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
let users = parse_etc_passwd(contents.as_slice()).map_err(|()| "Invalid passwd content")?;
let users = parse_etc_passwd(contents.as_slice()).map_err(|_| "Invalid passwd content")?;
let mut file = File::open("/etc/group").await?;
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
let groups = parse_etc_group(contents.as_slice()).map_err(|()| "Invalid group content")?;
let groups = parse_etc_group(contents.as_slice()).map_err(|_| "Invalid group content")?;
let id_iter = users
.iter()
@ -451,6 +451,7 @@ async fn main() -> ExitCode {
std::env::set_var("RUST_LOG", "debug");
}
#[allow(clippy::expect_used)]
tracing_forest::worker_task()
.set_global(true)
// Fall back to stderr
@ -699,7 +700,7 @@ async fn main() -> ExitCode {
let _ = unsafe { umask(before) };
// Pre-process /etc/passwd and /etc/group for nxset
if let Err(_) = process_etc_passwd_group(&cachelayer).await {
if process_etc_passwd_group(&cachelayer).await.is_err() {
error!("Failed to process system id providers");
return ExitCode::FAILURE
}
@ -800,7 +801,7 @@ async fn main() -> ExitCode {
break;
}
_ = inotify_rx.recv() => {
if let Err(_) = process_etc_passwd_group(&inotify_cachelayer).await {
if process_etc_passwd_group(&inotify_cachelayer).await.is_err() {
error!("Failed to process system id providers");
}
}
@ -860,30 +861,35 @@ async fn main() -> ExitCode {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore

View file

@ -30,7 +30,7 @@ pub fn do_setfscreatecon_for_path(
) -> Result<(), String> {
match labeler.look_up(&CString::new(path_raw.to_owned()).unwrap(), 0) {
Ok(context) => {
if let Err(_) = context.set_for_new_file_system_objects(true) {
if context.set_for_new_file_system_objects(true).is_err() {
return Err("Failed setting creation context home directory path".to_string());
}
Ok(())

View file

@ -292,6 +292,7 @@ async fn main() -> ExitCode {
let ceuid = get_effective_uid();
let cegid = get_effective_gid();
#[allow(clippy::expect_used)]
tracing_forest::worker_task()
.set_global(true)
// Fall back to stderr
@ -376,30 +377,36 @@ async fn main() -> ExitCode {
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::terminate();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
break
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::alarm();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::hangup();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined1();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore
}
Some(()) = async move {
let sigterm = tokio::signal::unix::SignalKind::user_defined2();
#[allow(clippy::unwrap_used)]
tokio::signal::unix::signal(sigterm).unwrap().recv().await
} => {
// Ignore

View file

@ -6,6 +6,7 @@ use std::path::Path;
#[cfg(all(target_family = "unix", feature = "selinux"))]
use crate::selinux_util;
use crate::unix_passwd::UnixIntegrationError;
use serde::Deserialize;
@ -191,7 +192,7 @@ impl KanidmUnixdConfig {
pub fn read_options_from_optional_config<P: AsRef<Path> + std::fmt::Debug>(
self,
config_path: P,
) -> Result<Self, ()> {
) -> Result<Self, UnixIntegrationError> {
debug!("Attempting to load configuration from {:#?}", &config_path);
let mut f = match File::open(&config_path) {
Ok(f) => {
@ -224,11 +225,15 @@ impl KanidmUnixdConfig {
};
let mut contents = String::new();
f.read_to_string(&mut contents)
.map_err(|e| eprintln!("{:?}", e))?;
f.read_to_string(&mut contents).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
let config: ConfigInt =
toml::from_str(contents.as_str()).map_err(|e| eprintln!("{:?}", e))?;
let config: ConfigInt = toml::from_str(contents.as_str()).map_err(|e| {
error!("{:?}", e);
UnixIntegrationError
})?;
// Now map the values into our config.
Ok(KanidmUnixdConfig {

View file

@ -16,15 +16,15 @@ pub struct EtcUser {
pub shell: String,
}
pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, ()> {
pub fn parse_etc_passwd(bytes: &[u8]) -> Result<Vec<EtcUser>, UnixIntegrationError> {
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.delimiter(b':')
.from_reader(bytes);
rdr.deserialize()
.map(|result| result.map_err(|_e| ()))
.collect::<Result<Vec<EtcUser>, ()>>()
.map(|result| result.map_err(|_e| UnixIntegrationError))
.collect::<Result<Vec<EtcUser>, UnixIntegrationError>>()
}
fn members<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
@ -60,15 +60,18 @@ pub struct EtcGroup {
pub members: Vec<String>,
}
pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, ()> {
#[derive(Debug)]
pub struct UnixIntegrationError;
pub fn parse_etc_group(bytes: &[u8]) -> Result<Vec<EtcGroup>, UnixIntegrationError> {
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new()
.has_headers(false)
.delimiter(b':')
.from_reader(bytes);
rdr.deserialize()
.map(|result| result.map_err(|_e| ()))
.collect::<Result<Vec<EtcGroup>, ()>>()
.map(|result| result.map_err(|_e| UnixIntegrationError))
.collect::<Result<Vec<EtcGroup>, UnixIntegrationError>>()
}
#[cfg(test)]

View file

@ -15,6 +15,7 @@ use kanidm_unix_common::unix_config::TpmPolicy;
use kanidmd_core::config::{Configuration, IntegrationTestConfig, ServerRole};
use kanidmd_core::create_server_core;
use tokio::task;
use tracing::log::debug;
static PORT_ALLOC: AtomicU16 = AtomicU16::new(28080);
const ADMIN_TEST_USER: &str = "admin";
@ -72,7 +73,7 @@ async fn setup_test(fix_fn: Fixture) -> (CacheLayer, KanidmClient) {
create_server_core(config, false)
.await
.expect("failed to start server core");
// We have to yield now to guarantee that the tide elements are setup.
// We have to yield now to guarantee that the elements are setup.
task::yield_now().await;
// Setup the client, and the address we selected.
@ -126,6 +127,7 @@ async fn test_fixture(rsclient: KanidmClient) {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
debug!("auth_simple_password res: {:?}", res);
assert!(res.is_ok());
// Not recommended in production!
rsclient
@ -664,10 +666,13 @@ async fn test_cache_nxcache() {
assert!(gt.is_none());
// Should all now be nxed
assert!(cachelayer
.check_nxcache(&Id::Name("oracle".to_string()))
.await
.is_some());
assert!(
cachelayer
.check_nxcache(&Id::Name("oracle".to_string()))
.await
.is_some(),
"'oracle' Wasn't in the nxcache!"
);
assert!(cachelayer.check_nxcache(&Id::Gid(2000)).await.is_some());
assert!(cachelayer
.check_nxcache(&Id::Name("oracle_group".to_string()))