mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
OpenAPI/swagger docs autogen (#2175)
* always be clippyin' * pulling oauth2 api things out into their own module * starting openapi generation
This commit is contained in:
parent
8bcf1935a5
commit
f28d5cef22
93
Cargo.lock
generated
93
Cargo.lock
generated
|
@ -2952,6 +2952,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
"webauthn-rs-proto",
|
||||
]
|
||||
|
@ -3081,7 +3082,10 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3158,6 +3162,7 @@ dependencies = [
|
|||
"compact_jwt",
|
||||
"fantoccini",
|
||||
"futures",
|
||||
"http",
|
||||
"hyper-tls",
|
||||
"kanidm_build_profiles",
|
||||
"kanidm_client",
|
||||
|
@ -4544,6 +4549,41 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "6.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-impl"
|
||||
version = "6.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-embed-utils",
|
||||
"shellexpand",
|
||||
"syn 2.0.38",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-embed-utils"
|
||||
version = "7.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74"
|
||||
dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
@ -5656,6 +5696,47 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d82b1bc5417102a73e8464c686eef947bdfb99fcdfc0a4f228e81afa9526470a"
|
||||
dependencies = [
|
||||
"indexmap 2.0.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84614caa239fb25b2bb373a52859ffd94605ceb256eeb1d63436325cf81e3653"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.4.1"
|
||||
|
@ -6317,6 +6398,18 @@ dependencies = [
|
|||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.12.4"
|
||||
|
|
|
@ -91,7 +91,6 @@ axum = { version = "0.6.20", features = [
|
|||
"form",
|
||||
"headers",
|
||||
"http2",
|
||||
"http2",
|
||||
"json",
|
||||
"macros",
|
||||
"multipart",
|
||||
|
@ -208,6 +207,7 @@ tss-esapi = "^7.3.0"
|
|||
url = "^2.4.1"
|
||||
urlencoding = "2.1.3"
|
||||
users = "^0.11.0"
|
||||
utoipa = "3.5.0"
|
||||
uuid = "^1.4.1"
|
||||
|
||||
wasm-bindgen = "^0.2.86"
|
||||
|
|
|
@ -135,14 +135,14 @@ reauthenticate for short periods to access higher levels of privilege.
|
|||
|
||||
When using a user command that requires these privileges you will be warned:
|
||||
|
||||
```
|
||||
```shell
|
||||
kanidm person credential update william
|
||||
# Privileges have expired for william@idm.example.com - you need to re-authenticate again.
|
||||
```
|
||||
|
||||
To reauthenticate
|
||||
|
||||
```
|
||||
```shell
|
||||
kanidm reauth -D william
|
||||
```
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ use std::os::unix::fs::MetadataExt;
|
|||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME};
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME, KOPID, KSESSIONID, KVERSION};
|
||||
use kanidm_proto::v1::*;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::Response;
|
||||
|
@ -47,10 +47,6 @@ mod service_account;
|
|||
mod sync_account;
|
||||
mod system;
|
||||
|
||||
pub const KOPID: &str = "X-KANIDM-OPID";
|
||||
pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID";
|
||||
|
||||
const KVERSION: &str = "X-KANIDM-VERSION";
|
||||
const EXPECT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -14,7 +14,6 @@ repository = { workspace = true }
|
|||
[features]
|
||||
wasm = ["webauthn-rs-proto/wasm"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
base32 = { workspace = true }
|
||||
base64urlsafedata = { workspace = true }
|
||||
|
@ -27,6 +26,6 @@ time = { workspace = true, features = ["serde", "std"] }
|
|||
tracing = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
urlencoding = { workspace = true }
|
||||
utoipa = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
webauthn-rs-proto = { workspace = true }
|
||||
|
||||
|
|
|
@ -191,3 +191,9 @@ pub const TEST_ATTR_TEST_ATTR: &str = "testattr";
|
|||
pub const TEST_ATTR_EXTRA: &str = "extra";
|
||||
pub const TEST_ATTR_NUMBER: &str = "testattrnumber";
|
||||
pub const TEST_ATTR_NOTALLOWED: &str = "notallowed";
|
||||
|
||||
pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID";
|
||||
pub const KOPID: &str = "X-KANIDM-OPID";
|
||||
pub const KVERSION: &str = "X-KANIDM-VERSION";
|
||||
|
||||
pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use base64urlsafedata::Base64UrlSafeData;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr};
|
||||
|
@ -12,7 +13,7 @@ use crate::constants::{
|
|||
ATTR_NAME, ATTR_SSH_PUBLICKEY,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||
pub enum ScimSyncState {
|
||||
Refresh,
|
||||
Active { cookie: Base64UrlSafeData },
|
||||
|
|
|
@ -9,6 +9,7 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use webauthn_rs_proto::{
|
||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
||||
|
@ -19,7 +20,7 @@ use crate::constants::{ATTR_GROUP, ATTR_LDAP_SSHPUBLICKEY};
|
|||
|
||||
// These proto implementations are here because they have public definitions
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, ToSchema)]
|
||||
pub enum AccountType {
|
||||
Person,
|
||||
ServiceAccount,
|
||||
|
@ -35,7 +36,7 @@ impl ToString for AccountType {
|
|||
}
|
||||
|
||||
/* ===== errors ===== */
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SchemaError {
|
||||
NotImplemented,
|
||||
|
@ -52,7 +53,7 @@ pub enum SchemaError {
|
|||
PhantomAttribute(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PluginError {
|
||||
AttrUnique(String),
|
||||
|
@ -62,7 +63,7 @@ pub enum PluginError {
|
|||
Oauth2Secrets,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ConsistencyError {
|
||||
Unknown,
|
||||
|
@ -88,7 +89,7 @@ pub enum ConsistencyError {
|
|||
DeniedName(Uuid),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum PasswordFeedback {
|
||||
// https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Suggestion.html
|
||||
|
@ -217,7 +218,7 @@ impl fmt::Display for PasswordFeedback {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OperationError {
|
||||
SessionExpired,
|
||||
|
@ -295,7 +296,7 @@ impl PartialEq for OperationError {
|
|||
// domain specific fields for the purposes of IDM, over the normal
|
||||
// entry/ava/filter types. These related deeply to schema.
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct Group {
|
||||
pub spn: String,
|
||||
pub uuid: String,
|
||||
|
@ -308,7 +309,7 @@ impl fmt::Display for Group {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct Claim {
|
||||
pub name: String,
|
||||
pub uuid: String,
|
||||
|
@ -380,7 +381,7 @@ impl fmt::Display for UatStatusState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UatStatus {
|
||||
pub account_id: Uuid,
|
||||
|
@ -426,7 +427,7 @@ pub enum UatPurpose {
|
|||
/// This structure and how it works will *very much* change over time from this
|
||||
/// point onward! This means on updates, that sessions will invalidate in many
|
||||
/// cases.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct UserAuthToken {
|
||||
pub session_id: Uuid,
|
||||
|
@ -499,7 +500,7 @@ pub enum ApiTokenPurpose {
|
|||
Synchronise,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ApiToken {
|
||||
// The account this is associated with.
|
||||
|
@ -546,7 +547,7 @@ impl PartialEq for ApiToken {
|
|||
|
||||
impl Eq for ApiToken {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ApiTokenGenerate {
|
||||
pub label: String,
|
||||
|
@ -560,7 +561,7 @@ pub struct ApiTokenGenerate {
|
|||
|
||||
// This is similar to uat, but omits claims (they have no role in radius), and adds
|
||||
// the radius secret field.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct RadiusAuthToken {
|
||||
pub name: String,
|
||||
pub displayname: String,
|
||||
|
@ -581,7 +582,7 @@ impl fmt::Display for RadiusAuthToken {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct UnixGroupToken {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
|
@ -598,13 +599,13 @@ impl fmt::Display for UnixGroupToken {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct GroupUnixExtend {
|
||||
pub gidnumber: Option<u32>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct UnixUserToken {
|
||||
pub name: String,
|
||||
pub spn: String,
|
||||
|
@ -639,7 +640,7 @@ impl fmt::Display for UnixUserToken {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AccountUnixExtend {
|
||||
pub gidnumber: Option<u32>,
|
||||
|
@ -665,7 +666,7 @@ pub enum CredentialDetailType {
|
|||
PasswordMfa(Vec<String>, Vec<String>, usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct CredentialDetail {
|
||||
pub uuid: Uuid,
|
||||
pub type_: CredentialDetailType,
|
||||
|
@ -729,13 +730,13 @@ impl fmt::Display for CredentialDetail {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct PasskeyDetail {
|
||||
pub uuid: Uuid,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct CredentialStatus {
|
||||
pub creds: Vec<CredentialDetail>,
|
||||
}
|
||||
|
@ -750,7 +751,7 @@ impl fmt::Display for CredentialStatus {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct BackupCodesView {
|
||||
pub backup_codes: Vec<String>,
|
||||
}
|
||||
|
@ -776,7 +777,7 @@ impl fmt::Display for Entry {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Filter {
|
||||
// This is attr - value
|
||||
|
@ -804,7 +805,7 @@ pub enum Modify {
|
|||
Purged(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||
pub struct ModifyList {
|
||||
pub mods: Vec<Modify>,
|
||||
}
|
||||
|
@ -815,7 +816,7 @@ impl ModifyList {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SearchRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
@ -826,7 +827,7 @@ impl SearchRequest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SearchResponse {
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
@ -837,7 +838,7 @@ impl SearchResponse {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateRequest {
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
@ -848,7 +849,7 @@ impl CreateRequest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DeleteRequest {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
@ -859,7 +860,7 @@ impl DeleteRequest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ModifyRequest {
|
||||
// Probably needs a modlist?
|
||||
pub filter: Filter,
|
||||
|
@ -884,7 +885,7 @@ impl ModifyRequest {
|
|||
//
|
||||
// On loginSuccess, we send a cookie, and that allows the token to be
|
||||
// generated. The cookie can be shared between servers.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthCredential {
|
||||
Anonymous,
|
||||
|
@ -909,7 +910,7 @@ impl fmt::Debug for AuthCredential {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialOrd, Ord)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialOrd, Ord, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthMech {
|
||||
Anonymous,
|
||||
|
@ -935,13 +936,13 @@ impl fmt::Display for AuthMech {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthIssueSession {
|
||||
Token,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthStep {
|
||||
// name
|
||||
|
@ -962,14 +963,14 @@ pub enum AuthStep {
|
|||
}
|
||||
|
||||
// Request auth for identity X with roles Y?
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AuthRequest {
|
||||
pub step: AuthStep,
|
||||
}
|
||||
|
||||
// Respond with the list of auth types and nonce, etc.
|
||||
// It can also contain a denied, or success.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthAllowed {
|
||||
Anonymous,
|
||||
|
@ -1032,7 +1033,7 @@ impl fmt::Display for AuthAllowed {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AuthState {
|
||||
// You need to select how you want to talk to me.
|
||||
|
@ -1050,7 +1051,7 @@ pub enum AuthState {
|
|||
// SuccessCookie,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AuthResponse {
|
||||
pub sessionid: Uuid,
|
||||
pub state: AuthState,
|
||||
|
@ -1072,7 +1073,7 @@ pub enum SetCredentialRequest {
|
|||
}
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TotpAlgo {
|
||||
Sha1,
|
||||
|
@ -1090,7 +1091,7 @@ impl fmt::Display for TotpAlgo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TotpSecret {
|
||||
pub accountname: String,
|
||||
/// User-facing name of the system, issuer of the TOTP
|
||||
|
@ -1123,12 +1124,12 @@ impl TotpSecret {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CUIntentToken {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CUSessionToken {
|
||||
pub token: String,
|
||||
}
|
||||
|
@ -1170,7 +1171,7 @@ impl fmt::Debug for CURequest {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum CURegState {
|
||||
// Nothing in progress.
|
||||
None,
|
||||
|
@ -1181,14 +1182,14 @@ pub enum CURegState {
|
|||
Passkey(CreationChallengeResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub enum CUExtPortal {
|
||||
None,
|
||||
Hidden,
|
||||
Some(Url),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CUStatus {
|
||||
// Display values
|
||||
pub spn: String,
|
||||
|
@ -1204,7 +1205,7 @@ pub struct CUStatus {
|
|||
pub passkeys_can_edit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
|
||||
pub struct WhoamiResponse {
|
||||
// Should we just embed the entry? Or destructure it?
|
||||
pub youare: Entry,
|
||||
|
@ -1217,7 +1218,7 @@ impl WhoamiResponse {
|
|||
}
|
||||
|
||||
// Simple string value provision.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SingleStringRequest {
|
||||
pub value: String,
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ filetime = { workspace = true }
|
|||
futures = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
http = { workspace = true }
|
||||
|
||||
hyper = { workspace = true }
|
||||
kanidm_proto = { workspace = true }
|
||||
kanidm_utils_users = { workspace = true }
|
||||
|
@ -63,6 +62,15 @@ urlencoding = { workspace = true }
|
|||
tempfile = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
utoipa = { workspace = true, features = [
|
||||
"axum_extras",
|
||||
"openapi_extensions",
|
||||
"preserve_order", # Preserve order of properties when serializing the schema for a component.
|
||||
] }
|
||||
utoipa-swagger-ui = { version = "3.1.5", features = ["axum"] }
|
||||
|
||||
[dev-dependencies]
|
||||
walkdir = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
kanidm_build_profiles = { workspace = true }
|
||||
|
|
|
@ -308,6 +308,12 @@ impl fmt::Display for Configuration {
|
|||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn new() -> Self {
|
||||
Configuration {
|
||||
address: DEFAULT_SERVER_ADDRESS.to_string(),
|
||||
ldapaddress: None,
|
||||
|
@ -335,12 +341,6 @@ impl Default for Configuration {
|
|||
integration_repl_config: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn new_for_test() -> Self {
|
||||
Configuration {
|
||||
|
|
249
server/core/src/https/apidocs/mod.rs
Normal file
249
server/core/src/https/apidocs/mod.rs
Normal file
|
@ -0,0 +1,249 @@
|
|||
use axum::{response::Redirect, routing::get, Router};
|
||||
use kanidm_proto::{scim_v1::ScimSyncState, v1};
|
||||
use utoipa::{
|
||||
openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
|
||||
Modify, OpenApi,
|
||||
};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use super::{errors::WebError, ServerState};
|
||||
|
||||
pub(crate) mod path_schema;
|
||||
pub(crate) mod response_schema;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
impl Modify for SecurityAddon {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
if let Some(components) = openapi.components.as_mut() {
|
||||
components.add_security_scheme(
|
||||
"token_jwt",
|
||||
SecurityScheme::Http(
|
||||
HttpBuilder::new()
|
||||
.scheme(HttpAuthScheme::Bearer)
|
||||
.bearer_format("JWT")
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// docs for the derive macro are here: <https://docs.rs/utoipa-gen/3.5.0/utoipa_gen/derive.OpenApi.html#info-attribute-syntax>
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
super::generic::status,
|
||||
super::generic::robots_txt,
|
||||
|
||||
super::oauth2::oauth2_image_get,
|
||||
|
||||
super::v1::raw_create,
|
||||
super::v1::raw_delete,
|
||||
super::v1::raw_modify,
|
||||
super::v1::raw_search,
|
||||
|
||||
super::v1_oauth2::oauth2_id_image_delete,
|
||||
super::v1_oauth2::oauth2_id_image_post,
|
||||
super::v1_oauth2::oauth2_get,
|
||||
super::v1_oauth2::oauth2_basic_post,
|
||||
super::v1_oauth2::oauth2_public_post,
|
||||
super::v1_oauth2::oauth2_id_get,
|
||||
super::v1_oauth2::oauth2_id_patch,
|
||||
super::v1_oauth2::oauth2_id_delete,
|
||||
super::v1_oauth2::oauth2_id_image_post,
|
||||
super::v1_oauth2::oauth2_id_image_delete,
|
||||
super::v1_oauth2::oauth2_id_get_basic_secret,
|
||||
super::v1_oauth2::oauth2_id_scopemap_post,
|
||||
super::v1_oauth2::oauth2_id_scopemap_delete,
|
||||
super::v1_oauth2::oauth2_id_sup_scopemap_post,
|
||||
super::v1_oauth2::oauth2_id_sup_scopemap_delete,
|
||||
super::v1_scim::scim_sync_post,
|
||||
super::v1_scim::scim_sync_get,
|
||||
|
||||
super::v1::schema_get,
|
||||
super::v1::whoami,
|
||||
super::v1::whoami_uat,
|
||||
super::v1::applinks_get,
|
||||
super::v1::schema_attributetype_get,
|
||||
super::v1::schema_attributetype_get_id,
|
||||
super::v1::schema_classtype_get,
|
||||
super::v1::schema_classtype_get_id,
|
||||
super::v1::person_get,
|
||||
super::v1::person_post,
|
||||
super::v1::service_account_credential_generate,
|
||||
super::v1::service_account_api_token_delete,
|
||||
super::v1::service_account_api_token_get,
|
||||
super::v1::service_account_api_token_post,
|
||||
super::v1::person_id_get,
|
||||
super::v1::person_id_patch,
|
||||
super::v1::person_id_delete,
|
||||
super::v1::person_id_get_attr,
|
||||
super::v1::person_id_put_attr,
|
||||
super::v1::person_id_post_attr,
|
||||
super::v1::person_id_delete_attr,
|
||||
super::v1::person_get_id_credential_status,
|
||||
super::v1::person_id_credential_update_get,
|
||||
super::v1::person_id_credential_update_intent_get,
|
||||
super::v1::person_id_credential_update_intent_ttl_get,
|
||||
|
||||
super::v1::service_account_id_ssh_pubkeys_get,
|
||||
super::v1::service_account_id_ssh_pubkeys_post,
|
||||
|
||||
super::v1::person_id_ssh_pubkeys_get,
|
||||
super::v1::person_id_ssh_pubkeys_post,
|
||||
super::v1::person_id_ssh_pubkeys_tag_get,
|
||||
super::v1::person_id_ssh_pubkeys_tag_delete,
|
||||
|
||||
super::v1::person_id_radius_get,
|
||||
super::v1::person_id_radius_post,
|
||||
super::v1::person_id_radius_delete,
|
||||
super::v1::person_id_radius_token_get,
|
||||
|
||||
super::v1::account_id_ssh_pubkeys_get,
|
||||
super::v1::account_id_radius_token_post,
|
||||
super::v1::service_account_id_unix_post,
|
||||
super::v1::person_id_unix_credential_put,
|
||||
super::v1::person_id_unix_credential_delete,
|
||||
super::v1::person_identify_user_post,
|
||||
super::v1::service_account_get,
|
||||
super::v1::service_account_post,
|
||||
super::v1::service_account_get,
|
||||
super::v1::service_account_post,
|
||||
super::v1::service_account_id_get,
|
||||
super::v1::service_account_id_delete,
|
||||
super::v1::service_account_id_get_attr,
|
||||
super::v1::service_account_id_put_attr,
|
||||
super::v1::service_account_id_post_attr,
|
||||
super::v1::service_account_id_delete_attr,
|
||||
super::v1::service_account_into_person,
|
||||
super::v1::service_account_api_token_post,
|
||||
super::v1::service_account_api_token_get,
|
||||
super::v1::service_account_api_token_delete,
|
||||
super::v1::service_account_credential_generate,
|
||||
super::v1::service_account_id_credential_status_get,
|
||||
super::v1::service_account_id_ssh_pubkeys_tag_get,
|
||||
super::v1::service_account_id_ssh_pubkeys_tag_delete,
|
||||
super::v1::account_id_unix_post,
|
||||
super::v1::account_id_unix_auth_post,
|
||||
super::v1::account_id_unix_token,
|
||||
super::v1::account_id_unix_token,
|
||||
super::v1::account_id_radius_token_post,
|
||||
super::v1::account_id_radius_token_get,
|
||||
super::v1::account_id_ssh_pubkeys_get,
|
||||
super::v1::account_id_ssh_pubkeys_tag_get,
|
||||
super::v1::account_id_user_auth_token_get,
|
||||
super::v1::account_user_auth_token_delete,
|
||||
super::v1::credential_update_exchange_intent,
|
||||
super::v1::credential_update_status,
|
||||
super::v1::credential_update_update,
|
||||
super::v1::credential_update_commit,
|
||||
super::v1::credential_update_cancel,
|
||||
super::v1::domain_get,
|
||||
super::v1::domain_attr_get,
|
||||
super::v1::domain_attr_put,
|
||||
super::v1::domain_attr_delete,
|
||||
super::v1::group_id_unix_token_get,
|
||||
super::v1::group_id_unix_post,
|
||||
super::v1::group_get,
|
||||
super::v1::group_post,
|
||||
super::v1::group_id_get,
|
||||
super::v1::group_id_delete,
|
||||
super::v1::group_id_attr_delete,
|
||||
super::v1::group_id_attr_get,
|
||||
super::v1::group_id_attr_put,
|
||||
super::v1::group_id_attr_post,
|
||||
super::v1::system_get,
|
||||
super::v1::system_attr_get,
|
||||
super::v1::system_attr_post,
|
||||
super::v1::system_attr_put,
|
||||
super::v1::system_attr_delete,
|
||||
super::v1::recycle_bin_get,
|
||||
super::v1::recycle_bin_id_get,
|
||||
super::v1::recycle_bin_revive_id_post,
|
||||
super::v1::auth,
|
||||
super::v1::auth_valid,
|
||||
super::v1::logout,
|
||||
super::v1::reauth,
|
||||
super::v1_scim::sync_account_get,
|
||||
super::v1_scim::sync_account_post,
|
||||
super::v1_scim::sync_account_id_get,
|
||||
super::v1_scim::sync_account_id_patch,
|
||||
super::v1_scim::sync_account_id_attr_get,
|
||||
super::v1_scim::sync_account_id_attr_put,
|
||||
super::v1_scim::sync_account_id_finalise_get,
|
||||
super::v1_scim::sync_account_id_terminate_get,
|
||||
super::v1_scim::sync_account_token_post,
|
||||
super::v1_scim::sync_account_token_delete,
|
||||
super::v1::debug_ipinfo,
|
||||
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
// TODO: can't add Entry/ProtoEntry to schema as this was only recently supported utoipa v3.5.0 doesn't support it - ref <https://github.com/juhaku/utoipa/pull/756/files>
|
||||
// v1::Entry,
|
||||
v1::AccountUnixExtend,
|
||||
v1::ApiToken,
|
||||
v1::ApiTokenGenerate,
|
||||
v1::AuthRequest,
|
||||
v1::AuthResponse,
|
||||
v1::AuthState,
|
||||
v1::BackupCodesView,
|
||||
v1::Claim,
|
||||
v1::CreateRequest,
|
||||
v1::CredentialDetail,
|
||||
v1::CredentialStatus,
|
||||
v1::CUIntentToken,
|
||||
v1::CUSessionToken,
|
||||
v1::CUStatus,
|
||||
v1::DeleteRequest,
|
||||
v1::Group,
|
||||
v1::GroupUnixExtend,
|
||||
v1::ModifyList,
|
||||
v1::ModifyRequest,
|
||||
v1::PasskeyDetail,
|
||||
v1::RadiusAuthToken,
|
||||
v1::SearchRequest,
|
||||
v1::SearchResponse,
|
||||
v1::SingleStringRequest,
|
||||
v1::TotpSecret,
|
||||
v1::TotpAlgo,
|
||||
v1::UatStatus,
|
||||
v1::UnixGroupToken,
|
||||
v1::UnixUserToken,
|
||||
v1::UserAuthToken,
|
||||
v1::WhoamiResponse,
|
||||
ScimSyncState,
|
||||
|
||||
WebError,
|
||||
|
||||
)
|
||||
),
|
||||
modifiers(&SecurityAddon),
|
||||
tags(
|
||||
(name = "kanidm", description = "Kanidm API")
|
||||
),
|
||||
info(
|
||||
title = "Kanidm",
|
||||
description = "API for interacting with the Kanidm system. This is a work in progress",
|
||||
contact( // <https://docs.rs/utoipa-gen/3.5.0/utoipa_gen/derive.OpenApi.html#info-attribute-syntax>
|
||||
name="Kanidm",
|
||||
url="https://github.com/kanidm/kanidm",
|
||||
)
|
||||
)
|
||||
)]
|
||||
pub(crate) struct ApiDoc;
|
||||
|
||||
pub(crate) fn router() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/docs", get(Redirect::temporary("/docs/swagger-ui")))
|
||||
.route("/docs/", get(Redirect::temporary("/docs/swagger-ui")))
|
||||
.merge(
|
||||
SwaggerUi::new("/docs/swagger-ui").url(
|
||||
"/docs/v1/openapi.json",
|
||||
<ApiDoc as utoipa::OpenApi>::openapi(),
|
||||
)
|
||||
)
|
||||
}
|
34
server/core/src/https/apidocs/path_schema.rs
Normal file
34
server/core/src/https/apidocs/path_schema.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
//! Path schema objects for the API documentation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::IntoParams;
|
||||
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct UuidOrName {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct TokenId {
|
||||
token_id: String,
|
||||
}
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct Id {
|
||||
id: String,
|
||||
}
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct Attr {
|
||||
attr: String,
|
||||
}
|
||||
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct RsName {
|
||||
// The short name of the OAuth2 resource server to target
|
||||
rs_name: String,
|
||||
}
|
||||
|
||||
#[derive(IntoParams, Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct GroupName {
|
||||
// The short name of the group to target
|
||||
group: String,
|
||||
}
|
56
server/core/src/https/apidocs/response_schema.rs
Normal file
56
server/core/src/https/apidocs/response_schema.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
//! This file contains the default response schemas for the API.
|
||||
//!
|
||||
//! These are used to generate the OpenAPI schema definitions.
|
||||
//!
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use std::collections::BTreeMap;
|
||||
use utoipa::{
|
||||
openapi::{Content, RefOr, Response, ResponseBuilder, ResponsesBuilder},
|
||||
IntoResponses,
|
||||
};
|
||||
|
||||
#[allow(dead_code)] // because this is used for the OpenAPI schema gen
|
||||
/// An empty response with `application/json` content type - use [ApiResponseWithout200] if you want to do everything but a 200
|
||||
pub(crate) enum DefaultApiResponse {
|
||||
Ok,
|
||||
InvalidRequest,
|
||||
NeedsAuthorization,
|
||||
NotAuthorized,
|
||||
}
|
||||
|
||||
impl IntoResponses for DefaultApiResponse {
|
||||
fn responses() -> BTreeMap<String, RefOr<Response>> {
|
||||
ResponsesBuilder::new()
|
||||
.response(
|
||||
"200",
|
||||
ResponseBuilder::new()
|
||||
.content(APPLICATION_JSON, Content::default())
|
||||
.description("Ok"),
|
||||
)
|
||||
.response("400", ResponseBuilder::new().description("Invalid Request"))
|
||||
.response("401", ResponseBuilder::new().description("Authorization required"))
|
||||
.response("403", ResponseBuilder::new().description("Not Authorized"))
|
||||
.build()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // because this is used for the OpenAPI schema gen
|
||||
/// A response set without the 200 status so the "defaults" can be handled.
|
||||
pub(crate) enum ApiResponseWithout200 {
|
||||
InvalidRequest,
|
||||
NeedsAuthorization,
|
||||
NotAuthorized,
|
||||
}
|
||||
|
||||
impl IntoResponses for ApiResponseWithout200 {
|
||||
fn responses() -> BTreeMap<String, RefOr<Response>> {
|
||||
ResponsesBuilder::new()
|
||||
.response("400", ResponseBuilder::new().description("Invalid Request"))
|
||||
.response("401", ResponseBuilder::new().description("Authorization required"))
|
||||
.response("403", ResponseBuilder::new().description("Not Authorized"))
|
||||
.build()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
113
server/core/src/https/apidocs/tests.rs
Normal file
113
server/core/src/https/apidocs/tests.rs
Normal file
|
@ -0,0 +1,113 @@
|
|||
#[test]
|
||||
/// This parses the source code trying to make sure we have API docs for every endpoint we publish.
|
||||
///
|
||||
/// It's not perfect, but it's a start!
|
||||
fn figure_out_if_we_have_all_the_routes() {
|
||||
use std::collections::HashMap;
|
||||
|
||||
// load this file
|
||||
let module_filename = format!("{}/src/https/apidocs/mod.rs", env!("CARGO_MANIFEST_DIR"));
|
||||
println!("trying to load apidocs source file: {}", module_filename);
|
||||
let file = std::fs::read_to_string(&module_filename).unwrap();
|
||||
|
||||
// find all the lines that start with super::v1:: and end with a comma
|
||||
let apidocs_function_finder = regex::Regex::new(r#"super::([a-zA-Z0-9_:]+),"#).unwrap();
|
||||
let mut apidocs_routes: HashMap<String, Vec<(String, String)>> = HashMap::new();
|
||||
for line in file.lines() {
|
||||
if let Some(caps) = apidocs_function_finder.captures(line) {
|
||||
let route = caps.get(1).unwrap().as_str();
|
||||
println!("route: {}", route);
|
||||
let mut splitter = route.split("::");
|
||||
|
||||
let module = splitter.next().unwrap();
|
||||
let handler = splitter.next().unwrap();
|
||||
if !apidocs_routes.contains_key(module) {
|
||||
apidocs_routes.insert(module.to_string(), Vec::new());
|
||||
}
|
||||
apidocs_routes
|
||||
.get_mut(module)
|
||||
.unwrap()
|
||||
.push((handler.to_string(), "unset".to_string()));
|
||||
}
|
||||
}
|
||||
for (module, routes) in apidocs_routes.iter() {
|
||||
println!("API Module: {}", module);
|
||||
for route in routes {
|
||||
println!(" - {} (method: {})", route.0, route.1);
|
||||
}
|
||||
}
|
||||
|
||||
// this looks for method(handler) axum things
|
||||
let routedef_finder =
|
||||
regex::Regex::new(r#"(any|delete|get|head|options|patch|post|put|trace)\(([a-z:_]+)\)"#)
|
||||
.unwrap();
|
||||
// work our way through the source files in this package looking for routedefs
|
||||
let mut found_routes: HashMap<String, Vec<(String, String)>> = HashMap::new();
|
||||
let walker = walkdir::WalkDir::new(format!("{}/src", env!("CARGO_MANIFEST_DIR")))
|
||||
.follow_links(false)
|
||||
.into_iter();
|
||||
|
||||
for entry in walker {
|
||||
let entry = entry.unwrap();
|
||||
if entry.path().is_dir() {
|
||||
continue;
|
||||
}
|
||||
println!("checking {}", entry.path().display());
|
||||
// because nobody wants to see their project dir all over the place
|
||||
let relative_filename = entry
|
||||
.path()
|
||||
.display()
|
||||
.to_string()
|
||||
.replace(&format!("{}/", env!("CARGO_MANIFEST_DIR")), "");
|
||||
|
||||
let source_module = relative_filename.split("/").last().unwrap();
|
||||
let source_module = source_module.split(".").next().unwrap();
|
||||
|
||||
let file = std::fs::read_to_string(&entry.path()).unwrap();
|
||||
for line in file.lines() {
|
||||
if line.contains("skip_route_check") {
|
||||
println!("Skipping this line because it contains skip_route_check");
|
||||
continue;
|
||||
}
|
||||
if let Some(caps) = routedef_finder.captures(line) {
|
||||
let method = caps.get(1).unwrap().as_str();
|
||||
let route = caps.get(2).unwrap().as_str();
|
||||
|
||||
if !found_routes.contains_key(source_module) {
|
||||
found_routes.insert(source_module.to_string(), Vec::new());
|
||||
}
|
||||
let new_route = (route.to_string(), method.to_string());
|
||||
println!("Found new route: {} {:?}", source_module, new_route);
|
||||
found_routes.get_mut(source_module).unwrap().push(new_route);
|
||||
}
|
||||
}
|
||||
}
|
||||
// now we check the things
|
||||
for (module, routes) in found_routes {
|
||||
if ["ui"].contains(&module.as_str()) {
|
||||
println!(
|
||||
"We can skip checking {} because it's allow-listed for docs",
|
||||
module
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if !apidocs_routes.contains_key(&module) {
|
||||
panic!("Module {} is missing from the API docs", module);
|
||||
}
|
||||
// we can't handle the method yet because that's in the derive
|
||||
for (route, _method) in routes {
|
||||
let mut found_route = false;
|
||||
for (apiroute_handler, _method) in apidocs_routes[&module].iter() {
|
||||
if &route == apiroute_handler {
|
||||
found_route = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_route {
|
||||
panic!("couldn't find apidocs route for {}::{}", module, route);
|
||||
} else {
|
||||
println!("Docs OK: {}::{}", module, route);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
69
server/core/src/https/errors.rs
Normal file
69
server/core/src/https/errors.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
//! Where we hide the error handling widgets
|
||||
//!
|
||||
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use http::header::ACCESS_CONTROL_ALLOW_ORIGIN;
|
||||
use http::{HeaderValue, StatusCode};
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// The web app's top level error type, this takes an `OperationError` and converts it into a HTTP response.
|
||||
#[derive(Debug, ToSchema)]
|
||||
pub enum WebError {
|
||||
/// Something went wrong when doing things.
|
||||
OperationError(OperationError),
|
||||
InternalServerError(String),
|
||||
}
|
||||
|
||||
impl From<OperationError> for WebError {
|
||||
fn from(inner: OperationError) -> Self {
|
||||
WebError::OperationError(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl WebError {
|
||||
pub(crate) fn response_with_access_control_origin_header(self) -> Response {
|
||||
let mut res = self.into_response();
|
||||
res.headers_mut().insert(
|
||||
ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||
HeaderValue::from_str("*").expect("Header generation failed, this is weird."),
|
||||
);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for WebError {
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
WebError::InternalServerError(inner) => {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, inner).into_response()
|
||||
}
|
||||
WebError::OperationError(inner) => {
|
||||
let (response_code, headers) = match &inner {
|
||||
OperationError::NotAuthenticated | OperationError::SessionExpired => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7235#section-4.1
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
// Some([("WWW-Authenticate", "Bearer")]),
|
||||
Some([("WWW-Authenticate", "Bearer"); 1]),
|
||||
)
|
||||
}
|
||||
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
||||
(StatusCode::FORBIDDEN, None)
|
||||
}
|
||||
OperationError::NoMatchingEntries => (StatusCode::NOT_FOUND, None),
|
||||
OperationError::PasswordQuality(_)
|
||||
| OperationError::EmptyRequest
|
||||
| OperationError::SchemaViolation(_) => (StatusCode::BAD_REQUEST, None),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, None),
|
||||
};
|
||||
let body =
|
||||
serde_json::to_string(&inner).unwrap_or_else(|_err| format!("{:?}", inner));
|
||||
match headers {
|
||||
Some(headers) => (response_code, headers, body).into_response(),
|
||||
None => (response_code, body).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,13 +4,14 @@ use axum::{
|
|||
http::{header::HeaderName, request::Parts, StatusCode},
|
||||
RequestPartsExt,
|
||||
};
|
||||
use kanidm_proto::constants::X_FORWARDED_FOR;
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
use crate::https::ServerState;
|
||||
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
|
||||
const X_FORWARDED_FOR_HEADER: HeaderName = HeaderName::from_static(X_FORWARDED_FOR);
|
||||
|
||||
pub struct TrustedClientIp(pub IpAddr);
|
||||
|
||||
|
@ -24,7 +25,7 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
|
|||
state: &ServerState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
if state.trust_x_forward_for {
|
||||
if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR) {
|
||||
if let Some(x_forward_for) = parts.headers.get(X_FORWARDED_FOR_HEADER) {
|
||||
// X forward for may be comma separate.
|
||||
let first = x_forward_for
|
||||
.to_str()
|
||||
|
|
|
@ -1,26 +1,45 @@
|
|||
use axum::extract::State;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Extension;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::get;
|
||||
use axum::{Extension, Router};
|
||||
use http::header::CONTENT_TYPE;
|
||||
use kanidmd_lib::status::StatusRequestEvent;
|
||||
|
||||
use super::middleware::KOpId;
|
||||
use super::ServerState;
|
||||
|
||||
/// Status endpoint used for healthchecks
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/status",
|
||||
responses(
|
||||
(status = 200, description = "Ok"),
|
||||
),
|
||||
tag = "system",
|
||||
|
||||
)]
|
||||
/// Status endpoint used for health checks, returns true when the server is up.
|
||||
pub async fn status(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
) -> String {
|
||||
let r = state
|
||||
.status_ref
|
||||
.handle_request(StatusRequestEvent {
|
||||
eventid: kopid.eventid,
|
||||
})
|
||||
.await;
|
||||
Response::new(format!("{}", r))
|
||||
format!("{}", r)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/robots.txt",
|
||||
responses(
|
||||
(status = 200, description = "Ok"),
|
||||
),
|
||||
tag = "ui",
|
||||
|
||||
)]
|
||||
pub async fn robots_txt() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "text/plain;charset=utf-8")],
|
||||
|
@ -31,3 +50,9 @@ pub async fn robots_txt() -> impl IntoResponse {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn route_setup() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/robots.txt", get(robots_txt))
|
||||
.route("/status", get(status))
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use axum::{
|
||||
headers::{CacheControl, HeaderMapExt},
|
||||
http::{self, Request},
|
||||
middleware::Next,
|
||||
response::Response,
|
||||
|
@ -18,3 +19,19 @@ pub async fn dont_cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
|
|||
|
||||
response
|
||||
}
|
||||
|
||||
/// Adds `no-cache max-age=0` to the response headers.
|
||||
pub async fn cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
let cache_header = CacheControl::new()
|
||||
.with_max_age(std::time::Duration::from_secs(300))
|
||||
.with_private();
|
||||
|
||||
response.headers_mut().typed_insert(cache_header);
|
||||
response.headers_mut().insert(
|
||||
http::header::PRAGMA,
|
||||
http::HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
|
||||
response
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ use axum::{
|
|||
TypedHeader,
|
||||
};
|
||||
use http::HeaderValue;
|
||||
use kanidm_proto::constants::{KOPID, KVERSION};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) mod caching;
|
||||
pub(crate) mod compression;
|
||||
pub(crate) mod hsts_header;
|
||||
|
@ -21,14 +21,16 @@ pub async fn version_middleware<B>(request: Request<B>, next: Next<B>) -> Respon
|
|||
let mut response = next.run(request).await;
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("X-KANIDM-VERSION", HeaderValue::from_static(KANIDM_VERSION));
|
||||
.insert(KVERSION, HeaderValue::from_static(KANIDM_VERSION));
|
||||
response
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// For holding onto the event ID and other handy request-based things
|
||||
pub struct KOpId {
|
||||
/// The event correlation ID
|
||||
pub eventid: Uuid,
|
||||
/// The User Access Token, if present
|
||||
pub uat: Option<String>,
|
||||
}
|
||||
|
||||
|
@ -45,7 +47,9 @@ pub async fn are_we_json_yet<B>(request: Request<B>, next: Next<B>) -> Response
|
|||
assert!(headers.contains_key(http::header::CONTENT_TYPE));
|
||||
assert!(
|
||||
headers.get(http::header::CONTENT_TYPE)
|
||||
== Some(&HeaderValue::from_static(crate::https::APPLICATION_JSON))
|
||||
== Some(&HeaderValue::from_static(
|
||||
kanidm_proto::constants::APPLICATION_JSON
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -72,7 +76,7 @@ pub async fn kopid_middleware<B>(
|
|||
// This conversion *should never* fail. If it does, rather than panic, we warn and
|
||||
// just don't put the id in the response.
|
||||
let _ = HeaderValue::from_str(&eventid.as_hyphenated().to_string())
|
||||
.map(|hv| response.headers_mut().insert("X-KANIDM-OPID", hv))
|
||||
.map(|hv| response.headers_mut().insert(KOPID, hv))
|
||||
.map_err(|err| {
|
||||
warn!(?err, "An invalid operation id was encountered");
|
||||
});
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
mod apidocs;
|
||||
pub(crate) mod errors;
|
||||
|
||||
mod extractors;
|
||||
mod generic;
|
||||
mod javascript;
|
||||
|
@ -8,35 +11,31 @@ mod tests;
|
|||
pub(crate) mod trace;
|
||||
mod ui;
|
||||
mod v1;
|
||||
mod v1_oauth2;
|
||||
mod v1_scim;
|
||||
|
||||
use self::generic::*;
|
||||
use self::javascript::*;
|
||||
use crate::actors::v1_read::QueryServerReadV1;
|
||||
use crate::actors::v1_write::QueryServerWriteV1;
|
||||
use crate::config::{Configuration, ServerRole, TlsConfiguration};
|
||||
use axum::extract::connect_info::{IntoMakeServiceWithConnectInfo, ResponseFuture};
|
||||
use axum::middleware::{from_fn, from_fn_with_state};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::*;
|
||||
use axum::Router;
|
||||
use axum_csp::{CspDirectiveType, CspValue};
|
||||
use axum_macros::FromRef;
|
||||
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
|
||||
use http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE};
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use hyper::server::accept::Accept;
|
||||
use hyper::server::conn::{AddrStream, Http};
|
||||
use hyper::Body;
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::constants::KSESSIONID;
|
||||
use kanidmd_lib::status::StatusActor;
|
||||
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod};
|
||||
use sketching::*;
|
||||
use tokio_openssl::SslStream;
|
||||
|
||||
use futures_util::future::poll_fn;
|
||||
use serde::Serialize;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::Level;
|
||||
|
||||
|
@ -82,7 +81,7 @@ impl ServerState {
|
|||
fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option<Uuid> {
|
||||
// We see if there is a signed header copy first.
|
||||
headers
|
||||
.get("X-KANIDM-AUTH-SESSION-ID")
|
||||
.get(KSESSIONID)
|
||||
.and_then(|hv| {
|
||||
// Get the first header value.
|
||||
hv.to_str().ok()
|
||||
|
@ -98,32 +97,39 @@ pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
|
|||
// let's set up the list of js module hashes
|
||||
{
|
||||
let filepath = "wasmloader.js";
|
||||
#[allow(clippy::unwrap_used)]
|
||||
js_files.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash: generate_integrity_hash(format!(
|
||||
match generate_integrity_hash(format!(
|
||||
"{}/{}",
|
||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||
filepath,
|
||||
))
|
||||
.unwrap(),
|
||||
)) {
|
||||
Ok(hash) => js_files.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash,
|
||||
filetype: Some("module".to_string()),
|
||||
});
|
||||
}),
|
||||
Err(err) => {
|
||||
admin_error!(?err, "Failed to generate integrity hash for wasmloader.js")
|
||||
}
|
||||
};
|
||||
}
|
||||
// let's set up the list of non-module hashes
|
||||
{
|
||||
let filepath = "external/bootstrap.bundle.min.js";
|
||||
#[allow(clippy::unwrap_used)]
|
||||
js_files.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash: generate_integrity_hash(format!(
|
||||
match generate_integrity_hash(format!(
|
||||
"{}/{}",
|
||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||
filepath,
|
||||
))
|
||||
.unwrap(),
|
||||
)) {
|
||||
Ok(hash) =>
|
||||
js_files.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash,
|
||||
filetype: None,
|
||||
});
|
||||
}),
|
||||
Err(err) => {
|
||||
admin_error!(?err, "Failed to generate integrity hash for bootstrap.bundle.min.js")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
js_files
|
||||
|
@ -197,28 +203,25 @@ pub async fn create_https_server(
|
|||
let static_routes = match config.role {
|
||||
ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => {
|
||||
// Create a spa router that captures everything at ui without key extraction.
|
||||
let spa_router = Router::new()
|
||||
.route("/", get(crate::https::ui::ui_handler))
|
||||
.fallback(crate::https::ui::ui_handler);
|
||||
|
||||
Router::new()
|
||||
// direct users to the base app page. If a login is required,
|
||||
// then views will take care of redirection. We shouldn't redir
|
||||
// to login because that force clears previous sessions!
|
||||
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
||||
.route("/manifest.webmanifest", get(manifest::manifest))
|
||||
.nest("/ui", spa_router)
|
||||
.route("/manifest.webmanifest", get(manifest::manifest)) // skip_route_check
|
||||
.nest("/ui", ui::spa_router())
|
||||
.layer(middleware::compression::new())
|
||||
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||
// skip_route_check
|
||||
}
|
||||
ServerRole::WriteReplicaNoUI => Router::new(),
|
||||
};
|
||||
let app = Router::new()
|
||||
.route("/robots.txt", get(robots_txt))
|
||||
.route("/status", get(status))
|
||||
.merge(oauth2::oauth2_route_setup(state.clone()))
|
||||
.merge(v1_scim::scim_route_setup())
|
||||
.merge(v1::router(state.clone()));
|
||||
.merge(generic::route_setup())
|
||||
.merge(oauth2::route_setup(state.clone()))
|
||||
.merge(v1_scim::route_setup())
|
||||
.merge(v1::route_setup(state.clone()));
|
||||
|
||||
let app = match config.role {
|
||||
ServerRole::WriteReplicaNoUI => app,
|
||||
|
@ -265,6 +268,7 @@ pub async fn create_https_server(
|
|||
// to be exited, and this middleware sets up ids' and other bits for for logging
|
||||
// coherence to be maintained.
|
||||
.layer(from_fn(middleware::kopid_middleware))
|
||||
.merge(apidocs::router())
|
||||
// this MUST be the last layer before with_state else the span never starts and everything breaks.
|
||||
.layer(trace_layer)
|
||||
.with_state(state)
|
||||
|
@ -402,85 +406,3 @@ pub(crate) async fn handle_conn(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert any kind of Result<T, OperationError> into an axum response with a stable type
|
||||
/// by JSON-encoding the body.
|
||||
#[instrument(name = "to_axum_response", level = "debug")]
|
||||
pub fn to_axum_response<T: Serialize + core::fmt::Debug>(
|
||||
v: Result<T, OperationError>,
|
||||
) -> Response<Body> {
|
||||
match v {
|
||||
Ok(iv) => {
|
||||
let body = match serde_json::to_string(&iv) {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
error!("Failed to serialise response: {:?}", err);
|
||||
format!("{:?}", iv)
|
||||
}
|
||||
};
|
||||
trace!("Response Body: {:?}", body);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, APPLICATION_JSON)
|
||||
.body(Body::from(body))
|
||||
.unwrap()
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("OperationError: {:?}", e);
|
||||
let res = match &e {
|
||||
OperationError::NotAuthenticated | OperationError::SessionExpired => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc7235#section-4.1
|
||||
Response::builder()
|
||||
.status(http::StatusCode::UNAUTHORIZED)
|
||||
.header("WWW-Authenticate", "Bearer")
|
||||
}
|
||||
OperationError::SystemProtectedObject | OperationError::AccessDenied => {
|
||||
Response::builder().status(http::StatusCode::FORBIDDEN)
|
||||
}
|
||||
OperationError::NoMatchingEntries => {
|
||||
Response::builder().status(http::StatusCode::NOT_FOUND)
|
||||
}
|
||||
OperationError::PasswordQuality(_)
|
||||
| OperationError::EmptyRequest
|
||||
| OperationError::SchemaViolation(_) => {
|
||||
Response::builder().status(http::StatusCode::BAD_REQUEST)
|
||||
}
|
||||
_ => Response::builder().status(http::StatusCode::INTERNAL_SERVER_ERROR),
|
||||
};
|
||||
match serde_json::to_string(&e) {
|
||||
#[allow(clippy::expect_used)]
|
||||
Ok(val) => res
|
||||
.body(Body::from(val))
|
||||
.expect("Failed to build response!"),
|
||||
#[allow(clippy::expect_used)]
|
||||
Err(_) => res
|
||||
.body(Body::from(format!("{:?}", e)))
|
||||
.expect("Failed to build response!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for the externally-defined error type from the protocol
|
||||
pub struct HttpOperationError(OperationError);
|
||||
|
||||
impl IntoResponse for HttpOperationError {
|
||||
fn into_response(self) -> Response {
|
||||
let HttpOperationError(error) = self;
|
||||
|
||||
let body = match serde_json::to_string(&error) {
|
||||
Ok(val) => val,
|
||||
Err(e) => {
|
||||
admin_warn!("Failed to serialize error response: original_error=\"{:?}\" serialization_error=\"{:?}\"", error , e);
|
||||
format!("{:?}", error)
|
||||
}
|
||||
};
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::from(body))
|
||||
.unwrap()
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use super::errors::WebError;
|
||||
use super::middleware::KOpId;
|
||||
use super::v1::{json_rest_event_get, json_rest_event_post};
|
||||
use super::{to_axum_response, HttpOperationError, ServerState};
|
||||
use super::ServerState;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::middleware::from_fn;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Extension, Form, Json, Router};
|
||||
use axum_macros::debug_handler;
|
||||
use compact_jwt::{JwkKeySet, OidcToken};
|
||||
use http::header::{
|
||||
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION, CONTENT_TYPE,
|
||||
LOCATION, WWW_AUTHENTICATE,
|
||||
|
@ -14,9 +15,7 @@ use http::header::{
|
|||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use hyper::Body;
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use kanidm_proto::internal::{ImageType, ImageValue};
|
||||
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse};
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse, AccessTokenResponse};
|
||||
use kanidmd_lib::idm::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
||||
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
|
||||
|
@ -24,9 +23,9 @@ use kanidmd_lib::idm::oauth2::{
|
|||
use kanidmd_lib::prelude::f_eq;
|
||||
use kanidmd_lib::prelude::*;
|
||||
use kanidmd_lib::value::PartialValue;
|
||||
use kanidmd_lib::valueset::image::ImageValueThings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// TODO: merge this into a value in WebError later
|
||||
pub struct HTTPOauth2Error(Oauth2Error);
|
||||
|
||||
impl IntoResponse for HTTPOauth2Error {
|
||||
|
@ -34,13 +33,14 @@ impl IntoResponse for HTTPOauth2Error {
|
|||
let HTTPOauth2Error(error) = self;
|
||||
|
||||
if let Oauth2Error::AuthenticationRequired = error {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.header(WWW_AUTHENTICATE, "Bearer")
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[
|
||||
(WWW_AUTHENTICATE, "Bearer"),
|
||||
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
|
||||
],
|
||||
)
|
||||
.into_response()
|
||||
} else {
|
||||
let err = ErrorResponse {
|
||||
error: error.to_string(),
|
||||
|
@ -54,299 +54,71 @@ impl IntoResponse for HTTPOauth2Error {
|
|||
format!("{:?}", err)
|
||||
}
|
||||
};
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::from(body))
|
||||
.unwrap()
|
||||
|
||||
}
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||
body,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == Oauth2 Configuration Endpoints ==
|
||||
|
||||
/// List all the OAuth2 Resource Servers
|
||||
pub async fn oauth2_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let filter = filter_all!(f_eq(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.into()
|
||||
));
|
||||
json_rest_event_get(state, None, filter, kopid).await
|
||||
}
|
||||
|
||||
pub async fn oauth2_basic_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> impl IntoResponse {
|
||||
let classes = vec![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerBasic.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
json_rest_event_post(state, classes, obj, kopid).await
|
||||
}
|
||||
|
||||
pub async fn oauth2_public_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> impl IntoResponse {
|
||||
let classes = vec![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerPublic.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
json_rest_event_post(state, classes, obj, kopid).await
|
||||
}
|
||||
|
||||
/// Get a filter matching a given OAuth2 Resource Server
|
||||
fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
||||
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
|
||||
filter_all!(f_and!([
|
||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||
f_eq(Attribute::OAuth2RsName, PartialValue::new_iname(rs_name))
|
||||
]))
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(rs_name): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_internalsearch(kopid.uat, filter, None, kopid.eventid)
|
||||
.await
|
||||
.map(|mut r| r.pop());
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip(state))]
|
||||
pub async fn oauth2_id_get_basic_secret(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/ui/images/oauth2/{rs_name}",
|
||||
params(
|
||||
super::apidocs::path_schema::RsName
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Ok", body=&[u8]),
|
||||
(status = 403, description = "Authorization refused"),
|
||||
(status = 403, description = "Authorization refused"),
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "ui",
|
||||
)]
|
||||
/// This returns the image for the OAuth2 Resource Server if the user has permissions
|
||||
///
|
||||
pub(crate) async fn oauth2_image_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_basic_secret_read(kopid.uat, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_patch(
|
||||
State(state): State<ServerState>,
|
||||
Path(rs_name): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_scopemap_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
Json(scopes): Json<Vec<String>>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_scopemap_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_scopemap_delete(kopid.uat, group, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_sup_scopemap_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
Json(scopes): Json<Vec<String>>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_sup_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_sup_scopemap_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_sup_scopemap_delete(kopid.uat, group, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Response<Body> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_internaldelete(kopid.uat, filter, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
/// this returns the image for the user if the user has permissions
|
||||
pub async fn oauth2_image_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Response<Body> {
|
||||
) -> Response {
|
||||
let rs_filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_rs_image_get_image(kopid.uat, rs_filter)
|
||||
.await;
|
||||
|
||||
let image = match res {
|
||||
Ok(image) => image,
|
||||
Err(_err) => {
|
||||
admin_error!(
|
||||
"Unable to get image for oauth2 resource server: {}",
|
||||
rs_name
|
||||
match res {
|
||||
Ok(image) => (
|
||||
StatusCode::OK,
|
||||
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
|
||||
image.contents,
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => {
|
||||
admin_debug!(
|
||||
"Unable to get image for oauth2 resource server {}: {:?}",
|
||||
rs_name,
|
||||
err
|
||||
);
|
||||
#[allow(clippy::unwrap_used)]
|
||||
return Response::builder()
|
||||
.status(StatusCode::NOT_FOUND)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
Response::builder()
|
||||
.header(CONTENT_TYPE, image.filetype.as_content_type_str())
|
||||
.body(Body::from(image.contents))
|
||||
.expect("Somehow failed to turn an image into a response!")
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_image_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Response<Body> {
|
||||
let rs_filter = oauth2_id(&rs_name);
|
||||
let res = state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_rs_image_delete(kopid.uat, rs_filter)
|
||||
.await;
|
||||
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
pub async fn oauth2_id_image_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> Response<Body> {
|
||||
// because we might not get an image
|
||||
let mut image: Option<ImageValue> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||
let filename = field.file_name().map(|f| f.to_string()).clone();
|
||||
if let Some(filename) = filename {
|
||||
let content_type = field.content_type().map(|f| f.to_string()).clone();
|
||||
|
||||
let content_type = match content_type {
|
||||
Some(val) => {
|
||||
if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
|
||||
val
|
||||
} else {
|
||||
debug!("Invalid content type: {}", val);
|
||||
let res =
|
||||
to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||
return res;
|
||||
// TODO: a 404 probably isn't perfect but it's not the worst
|
||||
(StatusCode::NOT_FOUND, "").into_response()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("No content type header provided");
|
||||
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||
return res;
|
||||
}
|
||||
};
|
||||
let data = match field.bytes().await {
|
||||
Ok(val) => val,
|
||||
Err(_e) => {
|
||||
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
let filetype = match ImageType::try_from_content_type(&content_type) {
|
||||
Ok(val) => val,
|
||||
Err(_err) => {
|
||||
let res = to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
image = Some(ImageValue {
|
||||
filetype,
|
||||
filename: filename.to_string(),
|
||||
contents: data.to_vec(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
let res = match image {
|
||||
Some(image) => {
|
||||
let image_validation_result = image.validate_image();
|
||||
if let Err(err) = image_validation_result {
|
||||
admin_error!("Invalid image uploaded: {:?}", err);
|
||||
return to_axum_response::<String>(Err(OperationError::InvalidRequestState));
|
||||
}
|
||||
|
||||
let rs_name = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_rs_image_update(kopid.uat, rs_name, image)
|
||||
.await
|
||||
}
|
||||
None => Err(OperationError::InvalidAttribute(
|
||||
"No image included, did you mean to use the DELETE method?".to_string(),
|
||||
)),
|
||||
};
|
||||
to_axum_response(res)
|
||||
}
|
||||
|
||||
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
|
||||
|
@ -701,7 +473,7 @@ pub async fn oauth2_token_post(
|
|||
Extension(kopid): Extension<KOpId>,
|
||||
headers: HeaderMap,
|
||||
Form(tok_req): Form<AccessTokenRequest>,
|
||||
) -> Result<Json<kanidm_proto::oauth2::AccessTokenResponse>, HTTPOauth2Error> {
|
||||
) -> Result<Json<AccessTokenResponse>, HTTPOauth2Error> {
|
||||
// This is called directly by the resource server, where we then issue
|
||||
// the token to the caller.
|
||||
|
||||
|
@ -731,7 +503,7 @@ pub async fn oauth2_openid_discovery_get(
|
|||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Result<Json<OidcDiscoveryResponse>, HttpOperationError> {
|
||||
) -> Result<Json<OidcDiscoveryResponse>, Response> {
|
||||
// let client_id = req.get_url_param("client_id")?;
|
||||
|
||||
let res = state
|
||||
|
@ -743,7 +515,7 @@ pub async fn oauth2_openid_discovery_get(
|
|||
Ok(dsc) => Ok(Json(dsc)),
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Unable to access discovery info");
|
||||
Err(HttpOperationError(e))
|
||||
Err(WebError::from(e).response_with_access_control_origin_header())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -753,7 +525,7 @@ pub async fn oauth2_openid_userinfo_get(
|
|||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<OidcToken>, HTTPOauth2Error> {
|
||||
// The token we want to inspect is in the authorisation header.
|
||||
let client_token = match kopid.uat {
|
||||
Some(val) => val,
|
||||
|
@ -778,13 +550,13 @@ pub async fn oauth2_openid_publickey_get(
|
|||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<Body> {
|
||||
to_axum_response(
|
||||
) -> Result<Json<JwkKeySet>, WebError> {
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_openid_publickey(client_id, kopid.eventid)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
/// This is called directly by the resource server, where we then issue
|
||||
|
@ -887,12 +659,12 @@ pub async fn oauth2_token_revoke_post(
|
|||
Some(val) => val,
|
||||
None =>
|
||||
{
|
||||
#[allow(clippy::unwrap_used)]
|
||||
return Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||
""
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -906,21 +678,17 @@ pub async fn oauth2_token_revoke_post(
|
|||
match res {
|
||||
Ok(()) =>
|
||||
{
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
(StatusCode::OK,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||
""
|
||||
).into_response()
|
||||
}
|
||||
Err(Oauth2Error::AuthenticationRequired) => {
|
||||
// This will trigger our ui to auth and retry.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::UNAUTHORIZED)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
(StatusCode::UNAUTHORIZED,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),],
|
||||
""
|
||||
).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
|
@ -928,30 +696,27 @@ pub async fn oauth2_token_revoke_post(
|
|||
error: e.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.body(Body::from(
|
||||
(StatusCode::BAD_REQUEST,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),],
|
||||
serde_json::to_string(&err).unwrap_or("".to_string()),
|
||||
))
|
||||
.unwrap()
|
||||
).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some requests from browsers require preflight so that CORS works.
|
||||
pub async fn oauth2_preflight_options() -> Response<Body> {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
||||
.header(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization")
|
||||
.body(Body::empty())
|
||||
.unwrap()
|
||||
pub async fn oauth2_preflight_options() -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
[
|
||||
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
|
||||
(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
|
||||
],
|
||||
String::new(),
|
||||
).into_response()
|
||||
}
|
||||
|
||||
pub fn oauth2_route_setup(state: ServerState) -> Router<ServerState> {
|
||||
pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
||||
// this has all the openid-related routes
|
||||
let openid_router = Router::new()
|
||||
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
|
@ -975,7 +740,7 @@ pub fn oauth2_route_setup(state: ServerState) -> Router<ServerState> {
|
|||
.with_state(state.clone());
|
||||
|
||||
Router::new()
|
||||
.route("/oauth2", get(oauth2_get))
|
||||
.route("/oauth2", get(super::v1_oauth2::oauth2_get))
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
.route(
|
||||
|
|
29
server/core/src/https/scim/sink.html
Normal file
29
server/core/src/https/scim/sink.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>🚰 Sink!</title>
|
||||
<meta name="theme-color" content="white" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" href="/pkg/img/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
___
|
||||
.' _ '.
|
||||
/ /` `\ \
|
||||
| | [__]
|
||||
| | {{
|
||||
| | }}
|
||||
_ | | _ {{
|
||||
___________<_>_| |_<_>}}________d
|
||||
.=======^=(___)=^={{====.
|
||||
/ .----------------}}---. \
|
||||
/ / {{ \ \
|
||||
/ / }} \ \
|
||||
( '=========================' )
|
||||
'-----------------------------'
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -1,13 +1,20 @@
|
|||
use axum::extract::State;
|
||||
use axum::http::HeaderValue;
|
||||
use axum::response::Response;
|
||||
use axum::Extension;
|
||||
use axum::routing::get;
|
||||
use axum::{Extension, Router};
|
||||
use http::header::CONTENT_TYPE;
|
||||
|
||||
use super::middleware::KOpId;
|
||||
use super::ServerState;
|
||||
|
||||
pub async fn ui_handler(
|
||||
pub(crate) fn spa_router() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/", get(ui_handler))
|
||||
.fallback(ui_handler)
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<String> {
|
||||
|
|
File diff suppressed because it is too large
Load diff
429
server/core/src/https/v1_oauth2.rs
Normal file
429
server/core/src/https/v1_oauth2.rs
Normal file
|
@ -0,0 +1,429 @@
|
|||
use super::apidocs::path_schema;
|
||||
use super::apidocs::response_schema::{DefaultApiResponse, ApiResponseWithout200};
|
||||
use super::errors::WebError;
|
||||
use super::middleware::KOpId;
|
||||
use super::oauth2::oauth2_id;
|
||||
use super::v1::{json_rest_event_get, json_rest_event_post};
|
||||
use super::ServerState;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use axum::{Extension, Json};
|
||||
use kanidm_proto::internal::{ImageType, ImageValue};
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidmd_lib::prelude::*;
|
||||
use kanidmd_lib::valueset::image::ImageValueThings;
|
||||
use sketching::admin_error;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/oauth2",
|
||||
responses(
|
||||
(status = 200,content_type="application/json", body=Vec<ProtoEntry>),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Lists all the OAuth2 Resource Servers
|
||||
pub(crate) async fn oauth2_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||
let filter = filter_all!(f_eq(
|
||||
Attribute::Class,
|
||||
EntryClass::OAuth2ResourceServer.into()
|
||||
));
|
||||
json_rest_event_get(state, None, filter, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/oauth2/basic",
|
||||
request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
// TODO: what does this actually do? :D
|
||||
pub(crate) async fn oauth2_basic_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let classes = vec![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerBasic.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
json_rest_event_post(state, classes, obj, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/oauth2/_public",
|
||||
request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
// TODO: what does this actually do? :D
|
||||
pub(crate) async fn oauth2_public_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let classes = vec![
|
||||
EntryClass::OAuth2ResourceServer.to_string(),
|
||||
EntryClass::OAuth2ResourceServerPublic.to_string(),
|
||||
EntryClass::Object.to_string(),
|
||||
];
|
||||
json_rest_event_post(state, classes, obj, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/oauth2/{rs_name}",
|
||||
params(
|
||||
path_schema::RsName
|
||||
),
|
||||
responses(
|
||||
(status = 200, /* TODO response=Option<ProtoEntry>*/),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Get the details of a given OAuth2 Resource Server.
|
||||
pub(crate) async fn oauth2_id_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(rs_name): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Result<Json<Option<ProtoEntry>>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_internalsearch(kopid.uat, filter, None, kopid.eventid)
|
||||
.await
|
||||
.map(|mut r| r.pop())
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/oauth2/{rs_name}/_basic_secret",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
),
|
||||
responses(
|
||||
(status = 200,content_type="application/json", body=Option<String>),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Get the basic secret for a given OAuth2 Resource Server. This is used for authentication.
|
||||
#[instrument(level = "info", skip(state))]
|
||||
pub(crate) async fn oauth2_id_get_basic_secret(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Result<Json<Option<String>>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_basic_secret_read(kopid.uat, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/oauth2/{rs_name}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
),
|
||||
request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Modify an OAuth2 Resource Server
|
||||
pub(crate) async fn oauth2_id_patch(
|
||||
State(state): State<ServerState>,
|
||||
Path(rs_name): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/oauth2/{rs_name}/_scopemap/{group}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
path_schema::GroupName,
|
||||
),
|
||||
request_body=Vec<String>,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Modify the scope map for a given OAuth2 Resource Server
|
||||
pub(crate) async fn oauth2_id_scopemap_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
Json(scopes): Json<Vec<String>>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/oauth2/{rs_name}/_scopemap/{group}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
path_schema::GroupName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
// Delete a scope map for a given OAuth2 Resource Server
|
||||
pub(crate) async fn oauth2_id_scopemap_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_scopemap_delete(kopid.uat, group, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
path_schema::GroupName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Create a supplemental scope map for a given OAuth2 Resource Server
|
||||
pub(crate) async fn oauth2_id_sup_scopemap_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
Json(scopes): Json<Vec<String>>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_sup_scopemap_update(kopid.uat, group, scopes, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/oauth2/{rs_name}/_sup_scopemap/{group}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
path_schema::GroupName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
// Delete a supplemental scope map configuration.
|
||||
pub(crate) async fn oauth2_id_sup_scopemap_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((rs_name, group)): Path<(String, String)>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_sup_scopemap_delete(kopid.uat, group, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/oauth2/{rs_name}",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
(status = 404),
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// Delete an OAuth2 Resource Server
|
||||
pub(crate) async fn oauth2_id_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_internaldelete(kopid.uat, filter, kopid.eventid)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/oauth2/{rs_name}/_image",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
// API endpoint for deleting the image associated with an OAuth2 Resource Server.
|
||||
pub(crate) async fn oauth2_id_image_delete(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_rs_image_delete(kopid.uat, oauth2_id(&rs_name))
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/oauth2/{rs_name}/_image",
|
||||
params(
|
||||
path_schema::RsName,
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/oauth2",
|
||||
)]
|
||||
/// API endpoint for creating/replacing the image associated with an OAuth2 Resource Server.
|
||||
///
|
||||
/// It requires a multipart form with the image file, and the content type must be one of the
|
||||
/// [VALID_IMAGE_UPLOAD_CONTENT_TYPES].
|
||||
pub(crate) async fn oauth2_id_image_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path(rs_name): Path<String>,
|
||||
mut multipart: axum::extract::Multipart,
|
||||
) -> Result<Json<()>, WebError> {
|
||||
// because we might not get an image
|
||||
let mut image: Option<ImageValue> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.unwrap_or(None) {
|
||||
let filename = field.file_name().map(|f| f.to_string()).clone();
|
||||
if let Some(filename) = filename {
|
||||
let content_type = field.content_type().map(|f| f.to_string()).clone();
|
||||
|
||||
let content_type = match content_type {
|
||||
Some(val) => {
|
||||
if VALID_IMAGE_UPLOAD_CONTENT_TYPES.contains(&val.as_str()) {
|
||||
val
|
||||
} else {
|
||||
debug!("Invalid content type: {}", val);
|
||||
return Err(OperationError::InvalidRequestState.into());
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("No content type header provided");
|
||||
return Err(OperationError::InvalidRequestState.into());
|
||||
}
|
||||
};
|
||||
let data = match field.bytes().await {
|
||||
Ok(val) => val,
|
||||
Err(_e) => return Err(OperationError::InvalidRequestState.into()),
|
||||
};
|
||||
|
||||
let filetype = match ImageType::try_from_content_type(&content_type) {
|
||||
Ok(val) => val,
|
||||
Err(_err) => return Err(OperationError::InvalidRequestState.into()),
|
||||
};
|
||||
|
||||
image = Some(ImageValue {
|
||||
filetype,
|
||||
filename: filename.to_string(),
|
||||
contents: data.to_vec(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
match image {
|
||||
Some(image) => {
|
||||
let image_validation_result = image.validate_image();
|
||||
match image_validation_result {
|
||||
Err(err) => {
|
||||
admin_error!("Invalid image uploaded: {:?}", err);
|
||||
Err(WebError::from(OperationError::InvalidRequestState))
|
||||
}
|
||||
Ok(_) => {
|
||||
let rs_name = oauth2_id(&rs_name);
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_oauth2_rs_image_update(kopid.uat, rs_name, image)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(WebError::from(OperationError::InvalidAttribute(
|
||||
"No image included, did you mean to use the DELETE method?".to_string(),
|
||||
))),
|
||||
}
|
||||
}
|
|
@ -1,190 +1,315 @@
|
|||
use super::apidocs::path_schema;
|
||||
use super::apidocs::response_schema::{ApiResponseWithout200,DefaultApiResponse};
|
||||
use super::errors::WebError;
|
||||
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::*;
|
||||
|
||||
use super::v1::{
|
||||
json_rest_event_get, json_rest_event_get_id, json_rest_event_get_id_attr, json_rest_event_post,
|
||||
json_rest_event_put_id_attr,
|
||||
};
|
||||
use super::ServerState;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::Html;
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Extension, Json, Router};
|
||||
use axum_auth::AuthBearer;
|
||||
use kanidm_proto::scim_v1::{ScimSyncRequest, ScimSyncState};
|
||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
||||
use kanidmd_lib::prelude::*;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sync_account",
|
||||
responses(
|
||||
(status = 200,content_type="application/json", body=Vec<ProtoEntry>),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
/// Get all? the sync accounts.
|
||||
pub async fn sync_account_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||
json_rest_event_get(state, None, filter, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sync_account",
|
||||
// request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
pub async fn sync_account_post(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Json(obj): Json<ProtoEntry>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let classes: Vec<String> = vec![EntryClass::SyncAccount.into(), EntryClass::Object.into()];
|
||||
json_rest_event_post(state, classes, obj, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sync_account/{id}",
|
||||
params(
|
||||
path_schema::Id
|
||||
),
|
||||
responses(
|
||||
(status = 200,content_type="application/json", body=Option<ProtoEntry>),
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
/// Get the details of a sync account
|
||||
pub async fn sync_account_id_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<Option<ProtoEntry>>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||
json_rest_event_get_id(state, id, filter, None, kopid).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/sync_account/{id}",
|
||||
params(
|
||||
path_schema::UuidOrName
|
||||
),
|
||||
request_body=ProtoEntry,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
/// Modify a sync account in-place
|
||||
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 {
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
|
||||
|
||||
let res = state
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
pub async fn sync_account_id_get_finalise(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sync_account/{id}/_finalise",
|
||||
params(
|
||||
path_schema::UuidOrName
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
// TODO: why is this a get and not a post?
|
||||
pub async fn sync_account_id_finalise_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let res = state
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_sync_account_finalise(kopid.uat, id, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
pub async fn sync_account_id_get_terminate(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sync_account/{id}/_terminate",
|
||||
params(
|
||||
path_schema::UuidOrName
|
||||
),
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
// TODO: why is this a get if it's a terminate?
|
||||
pub async fn sync_account_id_terminate_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let res = state
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_sync_account_terminate(kopid.uat, id, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sync_account/{id}/_sync_token",
|
||||
params(
|
||||
path_schema::UuidOrName
|
||||
),
|
||||
responses(
|
||||
(status = 200), // TODO: response content
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
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
|
||||
) -> Result<Json<String>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_sync_account_token_generate(kopid.uat, id, label, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/sync_account/{id}/_sync_token",
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
pub async fn sync_account_token_delete(
|
||||
State(state): State<ServerState>,
|
||||
Path(id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let res = state
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_sync_account_token_destroy(kopid.uat, id, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/scim/v1/Sync",
|
||||
request_body = ScimSyncRequest,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "scim",
|
||||
)]
|
||||
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
|
||||
) -> Result<Json<()>, WebError> {
|
||||
state
|
||||
.qe_w_ref
|
||||
.handle_scim_sync_apply(Some(bearer), changes, kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/scim/v1/Sync",
|
||||
responses(
|
||||
(status = 200), // TODO: response content
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "scim",
|
||||
)]
|
||||
async fn scim_sync_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
AuthBearer(bearer): AuthBearer,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<ScimSyncState>, WebError> {
|
||||
// Given the token, what is it's connected sync state?
|
||||
trace!(?bearer);
|
||||
let res = state
|
||||
state
|
||||
.qe_r_ref
|
||||
.handle_scim_sync_status(Some(bearer), kopid.eventid)
|
||||
.await;
|
||||
to_axum_response(res)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(WebError::from)
|
||||
}
|
||||
|
||||
pub async fn sync_account_id_get_attr(
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/sync_account/{id}/_attr/{attr}",
|
||||
params(
|
||||
path_schema::UuidOrName,
|
||||
path_schema::Attr,
|
||||
),
|
||||
responses(
|
||||
(status = 200), // TODO: response content
|
||||
ApiResponseWithout200,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
pub async fn sync_account_id_attr_get(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((id, attr)): Path<(String, String)>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<Option<Vec<String>>>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||
json_rest_event_get_id_attr(state, id, attr, filter, kopid).await
|
||||
}
|
||||
|
||||
pub async fn sync_account_id_put_attr(
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/sync_account/{id}/_attr/{attr}",
|
||||
params(
|
||||
path_schema::UuidOrName,
|
||||
path_schema::Attr,
|
||||
),
|
||||
request_body=Vec<String>,
|
||||
responses(
|
||||
DefaultApiResponse,
|
||||
),
|
||||
security(("token_jwt" = [])),
|
||||
tag = "v1/sync_account",
|
||||
)]
|
||||
pub async fn sync_account_id_attr_put(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
Path((id, attr)): Path<(String, String)>,
|
||||
Json(values): Json<Vec<String>>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Result<Json<()>, WebError> {
|
||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||
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>"#
|
||||
/// When you want the kitchen Sink
|
||||
async fn scim_sink_get() -> Html<&'static str> {
|
||||
Html::from(include_str!("scim/sink.html"))
|
||||
}
|
||||
|
||||
pub fn scim_route_setup() -> Router<ServerState> {
|
||||
pub fn route_setup() -> Router<ServerState> {
|
||||
Router::new()
|
||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
|
||||
//
|
||||
|
@ -254,6 +379,30 @@ pub fn scim_route_setup() -> Router<ServerState> {
|
|||
//
|
||||
// POST Send a sync update
|
||||
//
|
||||
.route(
|
||||
"/v1/sync_account",
|
||||
get(sync_account_get).post(sync_account_post),
|
||||
)
|
||||
.route(
|
||||
"/v1/sync_account/:id",
|
||||
get(sync_account_id_get).patch(sync_account_id_patch),
|
||||
)
|
||||
.route(
|
||||
"/v1/sync_account/:id/_attr/:attr",
|
||||
get(sync_account_id_attr_get).put(sync_account_id_attr_put),
|
||||
)
|
||||
.route(
|
||||
"/v1/sync_account/:id/_finalise",
|
||||
get(sync_account_id_finalise_get),
|
||||
)
|
||||
.route(
|
||||
"/v1/sync_account/:id/_terminate",
|
||||
get(sync_account_id_terminate_get),
|
||||
)
|
||||
.route(
|
||||
"/v1/sync_account/:id/_sync_token",
|
||||
post(sync_account_token_post).delete(sync_account_token_delete),
|
||||
)
|
||||
.route("/scim/v1/Sync", post(scim_sync_post).get(scim_sync_get))
|
||||
.route("/scim/v1/Sink", get(scim_sink_get))
|
||||
.route("/scim/v1/Sink", get(scim_sink_get)) // skip_route_check
|
||||
}
|
||||
|
|
|
@ -373,6 +373,7 @@ struct ConsumerConnSettings {
|
|||
replica_connect_timeout: Duration,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn repl_task(
|
||||
origin: Url,
|
||||
client_key: PKey<Private>,
|
||||
|
@ -827,7 +828,7 @@ async fn repl_acceptor(
|
|||
}
|
||||
|
||||
if respond.send(success).is_err() {
|
||||
warn!("Server certificate renewal was requested, but requsetor disconnected");
|
||||
warn!("Server certificate renewal was requested, but requester disconnected!");
|
||||
} else {
|
||||
trace!("Sent server certificate renewal status via control channel");
|
||||
}
|
||||
|
|
|
@ -278,11 +278,13 @@ impl fmt::Debug for FilterResolved {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A filter before it's gone through schema validation
|
||||
pub struct FilterInvalid {
|
||||
inner: FilterComp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||
/// A filter after it's gone through schema validation
|
||||
pub struct FilterValid {
|
||||
inner: FilterComp,
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ kanidmd_core = { workspace = true }
|
|||
kanidmd_lib = { workspace = true }
|
||||
# used for webdriver testing
|
||||
hyper-tls = { workspace = true }
|
||||
http = { workspace = true }
|
||||
# used for webdriver testing
|
||||
fantoccini = { version = "0.19.3", optional = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
@ -649,31 +649,19 @@ async fn test_https_robots_txt(rsclient: KanidmClient) {
|
|||
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY)
|
||||
);
|
||||
assert_ne!(
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY),
|
||||
None
|
||||
);
|
||||
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 response = match reqwest::get(rsclient.make_url("/v1/routemap")).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) {
|
||||
|
|
|
@ -20,6 +20,8 @@ async fn test_https_manifest(rsclient: KanidmClient) {
|
|||
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::{
|
|||
};
|
||||
|
||||
use kanidm_client::KanidmClient;
|
||||
use kanidm_proto::constants::X_FORWARDED_FOR;
|
||||
|
||||
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
|
@ -18,7 +19,7 @@ async fn dont_trust_xff_send_header(rsclient: KanidmClient) {
|
|||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header(
|
||||
"X-Forwarded-For",
|
||||
X_FORWARDED_FOR,
|
||||
"An invalid header that will get through!!!",
|
||||
)
|
||||
.send()
|
||||
|
@ -41,7 +42,7 @@ async fn dont_trust_xff_dont_send_header(rsclient: KanidmClient) {
|
|||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header(
|
||||
"X-Forwarded-For",
|
||||
X_FORWARDED_FOR,
|
||||
"An invalid header that will get through!!!",
|
||||
)
|
||||
.send()
|
||||
|
@ -69,7 +70,7 @@ async fn trust_xff_send_invalid_header_single_value(rsclient: KanidmClient) {
|
|||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header(
|
||||
"X-Forwarded-For",
|
||||
X_FORWARDED_FOR,
|
||||
"An invalid header that will get through!!!",
|
||||
)
|
||||
.send()
|
||||
|
@ -91,7 +92,7 @@ async fn trust_xff_send_invalid_header_multiple_values(rsclient: KanidmClient) {
|
|||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header(
|
||||
"X-Forwarded-For",
|
||||
X_FORWARDED_FOR,
|
||||
"203.0.113.195_noooo_my_ip_address, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
||||
)
|
||||
.send()
|
||||
|
@ -111,7 +112,7 @@ async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: KanidmClient)
|
|||
.unwrap();
|
||||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header("X-Forwarded-For", ip_addr)
|
||||
.header(X_FORWARDED_FOR, ip_addr)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -133,7 +134,7 @@ async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: KanidmClient)
|
|||
.unwrap();
|
||||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header("X-Forwarded-For", ip_addr)
|
||||
.header(X_FORWARDED_FOR, ip_addr)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -155,7 +156,7 @@ async fn trust_xff_send_valid_header_multiple_address(rsclient: KanidmClient) {
|
|||
.unwrap();
|
||||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header("X-Forwarded-For", first_ip_addr)
|
||||
.header(X_FORWARDED_FOR, first_ip_addr)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -177,7 +178,7 @@ async fn trust_xff_send_valid_header_multiple_address(rsclient: KanidmClient) {
|
|||
.unwrap();
|
||||
let res = client
|
||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||
.header("X-Forwarded-For", second_ip_addr)
|
||||
.header(X_FORWARDED_FOR, second_ip_addr)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -19,9 +19,16 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
|
|||
assert_eq!(response.status(), 200);
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY)
|
||||
);
|
||||
assert_ne!(
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY),
|
||||
None
|
||||
);
|
||||
assert_ne!(response.headers().get("content-security-policy"), None);
|
||||
|
||||
// here we test the /ui/login endpoint which should have the headers
|
||||
let response = match reqwest::get(rsclient.make_url("/ui/login")).await {
|
||||
|
@ -39,7 +46,14 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
|
|||
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get("content-security-policy")
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY)
|
||||
);
|
||||
assert_ne!(
|
||||
response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_SECURITY_POLICY),
|
||||
None
|
||||
);
|
||||
assert_ne!(response.headers().get("content-security-policy"), None);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ macro_rules! assert_no_cache {
|
|||
// Check we have correct nocache headers.
|
||||
let cache_header: &str = $response
|
||||
.headers()
|
||||
.get("cache-control")
|
||||
.get(http::header::CACHE_CONTROL)
|
||||
.expect("missing cache-control header")
|
||||
.to_str()
|
||||
.expect("invalid cache-control header");
|
||||
|
@ -151,9 +151,10 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
.expect("Failed to send discovery preflight request.");
|
||||
|
||||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
|
||||
let cors_header: &str = response
|
||||
.headers()
|
||||
.get("access-control-allow-origin")
|
||||
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.expect("missing access-control-allow-origin header")
|
||||
.to_str()
|
||||
.expect("invalid access-control-allow-origin header");
|
||||
|
@ -646,7 +647,7 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
|
|||
assert!(response.status() == reqwest::StatusCode::OK);
|
||||
let cors_header: &str = response
|
||||
.headers()
|
||||
.get("access-control-allow-origin")
|
||||
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||
.expect("missing access-control-allow-origin header")
|
||||
.to_str()
|
||||
.expect("invalid access-control-allow-origin header");
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
use std::path::Path;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use kanidm_proto::constants::KSESSIONID;
|
||||
use kanidm_proto::internal::ImageValue;
|
||||
use kanidm_proto::v1::{
|
||||
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
||||
|
@ -1826,7 +1827,7 @@ async fn start_password_session(
|
|||
};
|
||||
assert_eq!(res.status(), 200);
|
||||
|
||||
let session_id = res.headers().get("x-kanidm-auth-session-id").unwrap();
|
||||
let session_id = res.headers().get(KSESSIONID).unwrap();
|
||||
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Begin(AuthMech::Password),
|
||||
|
@ -1836,7 +1837,7 @@ async fn start_password_session(
|
|||
let res = match client
|
||||
.post(rsclient.make_url("/v1/auth"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-kanidm-auth-session-id", session_id)
|
||||
.header(KSESSIONID, session_id)
|
||||
.body(authreq)
|
||||
.send()
|
||||
.await
|
||||
|
@ -1854,7 +1855,7 @@ async fn start_password_session(
|
|||
let res = match client
|
||||
.post(rsclient.make_url("/v1/auth"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("x-kanidm-auth-session-id", session_id)
|
||||
.header(KSESSIONID, session_id)
|
||||
.body(authreq)
|
||||
.send()
|
||||
.await
|
||||
|
|
|
@ -105,6 +105,7 @@ async fn test_scim_sync_get(rsclient: KanidmClient) {
|
|||
.default_headers(headers)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// here we test the /ui/ endpoint which should have the headers
|
||||
let response = match client.get(rsclient.make_url("/scim/v1/Sync")).send().await {
|
||||
Ok(value) => value,
|
||||
|
@ -117,12 +118,30 @@ async fn test_scim_sync_get(rsclient: KanidmClient) {
|
|||
}
|
||||
};
|
||||
eprintln!("response: {:#?}", response);
|
||||
// assert_eq!(response.status(), 200);
|
||||
assert!(response.status().is_client_error());
|
||||
|
||||
// check that the CSP headers are coming back
|
||||
eprintln!(
|
||||
"csp headers: {:#?}",
|
||||
response.headers().get(http::header::CONTENT_SECURITY_POLICY)
|
||||
);
|
||||
assert_ne!(response.headers().get(http::header::CONTENT_SECURITY_POLICY), None);
|
||||
|
||||
// test that the proper content type comes back
|
||||
let url = rsclient.make_url("/scim/v1/Sink");
|
||||
let response = match client.get(url.clone()).send().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
panic!(
|
||||
"Failed to query {:?} : {:#?}",
|
||||
url,
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
assert!( response.status().is_success());
|
||||
let content_type = response.headers().get(http::header::CONTENT_TYPE).unwrap();
|
||||
assert!(content_type.to_str().unwrap().contains("text/html"));
|
||||
assert!(response.text().await.unwrap().contains("Sink"));
|
||||
|
||||
// eprintln!(
|
||||
// "csp headers: {:#?}",
|
||||
// response.headers().get("content-security-policy")
|
||||
// );
|
||||
// assert_ne!(response.headers().get("content-security-policy"), None);
|
||||
// eprintln!("{}", response.text().await.unwrap());
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
use error::FetchError;
|
||||
use gloo::console;
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, KSESSIONID};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
|
@ -91,7 +91,7 @@ pub async fn do_request(
|
|||
if let Some(sessionid) = models::pop_auth_session_id() {
|
||||
request
|
||||
.headers()
|
||||
.set("x-kanidm-auth-session-id", &sessionid)
|
||||
.set(KSESSIONID, &sessionid)
|
||||
.expect_throw("failed to set auth session id header");
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ pub async fn do_request(
|
|||
let status = resp.status();
|
||||
let headers: Headers = resp.headers();
|
||||
|
||||
if let Some(sessionid) = headers.get("x-kanidm-auth-session-id").ok().flatten() {
|
||||
if let Some(sessionid) = headers.get(KSESSIONID).ok().flatten() {
|
||||
models::push_auth_session_id(sessionid);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue