mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 04:27:02 +01:00
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:
parent
17fa61ceeb
commit
cc35654388
986
Cargo.lock
generated
986
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -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
|
||||
|
|
31
Makefile
31
Makefile
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
7
book/src/developers/designs/content_security_policy.md
Normal file
7
book/src/developers/designs/content_security_policy.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Content-Security-Policy Headers
|
||||
|
||||
These are required on any path which browser-based clients access.
|
||||
|
||||
- `/`
|
||||
- `/ui/`
|
||||
- `/pkg/`
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
64
scripts/test_coverage.sh
Executable 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.*"
|
|
@ -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" ] }
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>,
|
||||
|
|
33
server/core/src/https/generic.rs
Normal file
33
server/core/src/https/generic.rs
Normal 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: /
|
||||
"#,
|
||||
),
|
||||
)
|
||||
}
|
57
server/core/src/https/javascript.rs
Normal file
57
server/core/src/https/javascript.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
20
server/core/src/https/middleware/caching.rs
Normal file
20
server/core/src/https/middleware/caching.rs
Normal 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
|
||||
}
|
25
server/core/src/https/middleware/compression.rs
Normal file
25
server/core/src/https/middleware/compression.rs
Normal 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)
|
||||
}
|
21
server/core/src/https/middleware/csp_headers.rs
Normal file
21
server/core/src/https/middleware/csp_headers.rs
Normal 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
|
||||
}
|
89
server/core/src/https/middleware/mod.rs
Normal file
89
server/core/src/https/middleware/mod.rs
Normal 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
|
@ -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 });
|
||||
}
|
||||
}
|
23
server/core/src/https/tests.rs
Normal file
23
server/core/src/https/tests.rs
Normal 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>"#
|
||||
);
|
||||
}
|
65
server/core/src/https/ui.rs
Normal file
65
server/core/src/https/ui.rs
Normal 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
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1923,7 +1923,7 @@ impl Schema {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test))]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn write_blocking(&self) -> SchemaWriteTransaction<'_> {
|
||||
self.write()
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
22
server/testkit/tests/http_manifest.rs
Normal file
22
server/testkit/tests/http_manifest.rs
Normal 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")
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
32
server/testkit/tests/person.rs
Normal file
32
server/testkit/tests/person.rs
Normal 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);
|
||||
}
|
681
server/testkit/tests/routes.rs
Normal file
681
server/testkit/tests/routes.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
51
server/testkit/tests/self.rs
Normal file
51
server/testkit/tests/self.rs
Normal 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);
|
||||
}
|
30
server/testkit/tests/service_account.rs
Normal file
30
server/testkit/tests/service_account.rs
Normal 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);
|
||||
}
|
29
server/testkit/tests/system.rs
Normal file
29
server/testkit/tests/system.rs
Normal 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);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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()))
|
||||
|
|
Loading…
Reference in a new issue