mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-24 04:57: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",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"utoipa",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webauthn-rs-proto",
|
"webauthn-rs-proto",
|
||||||
]
|
]
|
||||||
|
@ -3081,7 +3082,10 @@ dependencies = [
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3158,6 +3162,7 @@ dependencies = [
|
||||||
"compact_jwt",
|
"compact_jwt",
|
||||||
"fantoccini",
|
"fantoccini",
|
||||||
"futures",
|
"futures",
|
||||||
|
"http",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"kanidm_build_profiles",
|
"kanidm_build_profiles",
|
||||||
"kanidm_client",
|
"kanidm_client",
|
||||||
|
@ -4544,6 +4549,41 @@ dependencies = [
|
||||||
"smallvec",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -5656,6 +5696,47 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
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]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
@ -6317,6 +6398,18 @@ dependencies = [
|
||||||
"syn 2.0.38",
|
"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]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
|
|
|
@ -91,7 +91,6 @@ axum = { version = "0.6.20", features = [
|
||||||
"form",
|
"form",
|
||||||
"headers",
|
"headers",
|
||||||
"http2",
|
"http2",
|
||||||
"http2",
|
|
||||||
"json",
|
"json",
|
||||||
"macros",
|
"macros",
|
||||||
"multipart",
|
"multipart",
|
||||||
|
@ -208,6 +207,7 @@ tss-esapi = "^7.3.0"
|
||||||
url = "^2.4.1"
|
url = "^2.4.1"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
users = "^0.11.0"
|
users = "^0.11.0"
|
||||||
|
utoipa = "3.5.0"
|
||||||
uuid = "^1.4.1"
|
uuid = "^1.4.1"
|
||||||
|
|
||||||
wasm-bindgen = "^0.2.86"
|
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:
|
When using a user command that requires these privileges you will be warned:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
kanidm person credential update william
|
kanidm person credential update william
|
||||||
# Privileges have expired for william@idm.example.com - you need to re-authenticate again.
|
# Privileges have expired for william@idm.example.com - you need to re-authenticate again.
|
||||||
```
|
```
|
||||||
|
|
||||||
To reauthenticate
|
To reauthenticate
|
||||||
|
|
||||||
```
|
```shell
|
||||||
kanidm reauth -D william
|
kanidm reauth -D william
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
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 kanidm_proto::v1::*;
|
||||||
use reqwest::header::CONTENT_TYPE;
|
use reqwest::header::CONTENT_TYPE;
|
||||||
use reqwest::Response;
|
use reqwest::Response;
|
||||||
|
@ -47,10 +47,6 @@ mod service_account;
|
||||||
mod sync_account;
|
mod sync_account;
|
||||||
mod system;
|
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");
|
const EXPECT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -14,7 +14,6 @@ repository = { workspace = true }
|
||||||
[features]
|
[features]
|
||||||
wasm = ["webauthn-rs-proto/wasm"]
|
wasm = ["webauthn-rs-proto/wasm"]
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base32 = { workspace = true }
|
base32 = { workspace = true }
|
||||||
base64urlsafedata = { workspace = true }
|
base64urlsafedata = { workspace = true }
|
||||||
|
@ -27,6 +26,6 @@ time = { workspace = true, features = ["serde", "std"] }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
urlencoding = { workspace = true }
|
urlencoding = { workspace = true }
|
||||||
|
utoipa = { workspace = true }
|
||||||
uuid = { workspace = true, features = ["serde"] }
|
uuid = { workspace = true, features = ["serde"] }
|
||||||
webauthn-rs-proto = { workspace = true }
|
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_EXTRA: &str = "extra";
|
||||||
pub const TEST_ATTR_NUMBER: &str = "testattrnumber";
|
pub const TEST_ATTR_NUMBER: &str = "testattrnumber";
|
||||||
pub const TEST_ATTR_NOTALLOWED: &str = "notallowed";
|
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 base64urlsafedata::Base64UrlSafeData;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr};
|
pub use scim_proto::prelude::{ScimAttr, ScimComplexAttr, ScimEntry, ScimError, ScimSimpleAttr};
|
||||||
|
@ -12,7 +13,7 @@ use crate::constants::{
|
||||||
ATTR_NAME, ATTR_SSH_PUBLICKEY,
|
ATTR_NAME, ATTR_SSH_PUBLICKEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||||
pub enum ScimSyncState {
|
pub enum ScimSyncState {
|
||||||
Refresh,
|
Refresh,
|
||||||
Active { cookie: Base64UrlSafeData },
|
Active { cookie: Base64UrlSafeData },
|
||||||
|
|
|
@ -9,6 +9,7 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use utoipa::ToSchema;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs_proto::{
|
use webauthn_rs_proto::{
|
||||||
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
|
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
|
// These proto implementations are here because they have public definitions
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, ToSchema)]
|
||||||
pub enum AccountType {
|
pub enum AccountType {
|
||||||
Person,
|
Person,
|
||||||
ServiceAccount,
|
ServiceAccount,
|
||||||
|
@ -35,7 +36,7 @@ impl ToString for AccountType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== errors ===== */
|
/* ===== errors ===== */
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum SchemaError {
|
pub enum SchemaError {
|
||||||
NotImplemented,
|
NotImplemented,
|
||||||
|
@ -52,7 +53,7 @@ pub enum SchemaError {
|
||||||
PhantomAttribute(String),
|
PhantomAttribute(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum PluginError {
|
pub enum PluginError {
|
||||||
AttrUnique(String),
|
AttrUnique(String),
|
||||||
|
@ -62,7 +63,7 @@ pub enum PluginError {
|
||||||
Oauth2Secrets,
|
Oauth2Secrets,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ConsistencyError {
|
pub enum ConsistencyError {
|
||||||
Unknown,
|
Unknown,
|
||||||
|
@ -88,7 +89,7 @@ pub enum ConsistencyError {
|
||||||
DeniedName(Uuid),
|
DeniedName(Uuid),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum PasswordFeedback {
|
pub enum PasswordFeedback {
|
||||||
// https://docs.rs/zxcvbn/latest/zxcvbn/feedback/enum.Suggestion.html
|
// 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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum OperationError {
|
pub enum OperationError {
|
||||||
SessionExpired,
|
SessionExpired,
|
||||||
|
@ -295,7 +296,7 @@ impl PartialEq for OperationError {
|
||||||
// domain specific fields for the purposes of IDM, over the normal
|
// domain specific fields for the purposes of IDM, over the normal
|
||||||
// entry/ava/filter types. These related deeply to schema.
|
// entry/ava/filter types. These related deeply to schema.
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct Group {
|
pub struct Group {
|
||||||
pub spn: String,
|
pub spn: String,
|
||||||
pub uuid: 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 struct Claim {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub uuid: 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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub struct UatStatus {
|
pub struct UatStatus {
|
||||||
pub account_id: Uuid,
|
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
|
/// 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
|
/// point onward! This means on updates, that sessions will invalidate in many
|
||||||
/// cases.
|
/// cases.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub struct UserAuthToken {
|
pub struct UserAuthToken {
|
||||||
pub session_id: Uuid,
|
pub session_id: Uuid,
|
||||||
|
@ -499,7 +500,7 @@ pub enum ApiTokenPurpose {
|
||||||
Synchronise,
|
Synchronise,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub struct ApiToken {
|
pub struct ApiToken {
|
||||||
// The account this is associated with.
|
// The account this is associated with.
|
||||||
|
@ -546,7 +547,7 @@ impl PartialEq for ApiToken {
|
||||||
|
|
||||||
impl Eq for ApiToken {}
|
impl Eq for ApiToken {}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub struct ApiTokenGenerate {
|
pub struct ApiTokenGenerate {
|
||||||
pub label: String,
|
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
|
// This is similar to uat, but omits claims (they have no role in radius), and adds
|
||||||
// the radius secret field.
|
// the radius secret field.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct RadiusAuthToken {
|
pub struct RadiusAuthToken {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub displayname: 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 struct UnixGroupToken {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub spn: 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 struct GroupUnixExtend {
|
||||||
pub gidnumber: Option<u32>,
|
pub gidnumber: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct UnixUserToken {
|
pub struct UnixUserToken {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub spn: 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)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AccountUnixExtend {
|
pub struct AccountUnixExtend {
|
||||||
pub gidnumber: Option<u32>,
|
pub gidnumber: Option<u32>,
|
||||||
|
@ -665,7 +666,7 @@ pub enum CredentialDetailType {
|
||||||
PasswordMfa(Vec<String>, Vec<String>, usize),
|
PasswordMfa(Vec<String>, Vec<String>, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct CredentialDetail {
|
pub struct CredentialDetail {
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub type_: CredentialDetailType,
|
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 struct PasskeyDetail {
|
||||||
pub uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
pub tag: String,
|
pub tag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct CredentialStatus {
|
pub struct CredentialStatus {
|
||||||
pub creds: Vec<CredentialDetail>,
|
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 struct BackupCodesView {
|
||||||
pub backup_codes: Vec<String>,
|
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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Filter {
|
pub enum Filter {
|
||||||
// This is attr - value
|
// This is attr - value
|
||||||
|
@ -804,7 +805,7 @@ pub enum Modify {
|
||||||
Purged(String),
|
Purged(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
|
||||||
pub struct ModifyList {
|
pub struct ModifyList {
|
||||||
pub mods: Vec<Modify>,
|
pub mods: Vec<Modify>,
|
||||||
}
|
}
|
||||||
|
@ -815,7 +816,7 @@ impl ModifyList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SearchRequest {
|
pub struct SearchRequest {
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
}
|
}
|
||||||
|
@ -826,7 +827,7 @@ impl SearchRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SearchResponse {
|
pub struct SearchResponse {
|
||||||
pub entries: Vec<Entry>,
|
pub entries: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
@ -837,7 +838,7 @@ impl SearchResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct CreateRequest {
|
pub struct CreateRequest {
|
||||||
pub entries: Vec<Entry>,
|
pub entries: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
@ -848,7 +849,7 @@ impl CreateRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct DeleteRequest {
|
pub struct DeleteRequest {
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
}
|
}
|
||||||
|
@ -859,7 +860,7 @@ impl DeleteRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ModifyRequest {
|
pub struct ModifyRequest {
|
||||||
// Probably needs a modlist?
|
// Probably needs a modlist?
|
||||||
pub filter: Filter,
|
pub filter: Filter,
|
||||||
|
@ -884,7 +885,7 @@ impl ModifyRequest {
|
||||||
//
|
//
|
||||||
// On loginSuccess, we send a cookie, and that allows the token to be
|
// On loginSuccess, we send a cookie, and that allows the token to be
|
||||||
// generated. The cookie can be shared between servers.
|
// generated. The cookie can be shared between servers.
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthCredential {
|
pub enum AuthCredential {
|
||||||
Anonymous,
|
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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthMech {
|
pub enum AuthMech {
|
||||||
Anonymous,
|
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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthIssueSession {
|
pub enum AuthIssueSession {
|
||||||
Token,
|
Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthStep {
|
pub enum AuthStep {
|
||||||
// name
|
// name
|
||||||
|
@ -962,14 +963,14 @@ pub enum AuthStep {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request auth for identity X with roles Y?
|
// Request auth for identity X with roles Y?
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct AuthRequest {
|
pub struct AuthRequest {
|
||||||
pub step: AuthStep,
|
pub step: AuthStep,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respond with the list of auth types and nonce, etc.
|
// Respond with the list of auth types and nonce, etc.
|
||||||
// It can also contain a denied, or success.
|
// It can also contain a denied, or success.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthAllowed {
|
pub enum AuthAllowed {
|
||||||
Anonymous,
|
Anonymous,
|
||||||
|
@ -1032,7 +1033,7 @@ impl fmt::Display for AuthAllowed {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum AuthState {
|
pub enum AuthState {
|
||||||
// You need to select how you want to talk to me.
|
// You need to select how you want to talk to me.
|
||||||
|
@ -1050,7 +1051,7 @@ pub enum AuthState {
|
||||||
// SuccessCookie,
|
// SuccessCookie,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct AuthResponse {
|
pub struct AuthResponse {
|
||||||
pub sessionid: Uuid,
|
pub sessionid: Uuid,
|
||||||
pub state: AuthState,
|
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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum TotpAlgo {
|
pub enum TotpAlgo {
|
||||||
Sha1,
|
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 struct TotpSecret {
|
||||||
pub accountname: String,
|
pub accountname: String,
|
||||||
/// User-facing name of the system, issuer of the TOTP
|
/// 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 struct CUIntentToken {
|
||||||
pub token: String,
|
pub token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct CUSessionToken {
|
pub struct CUSessionToken {
|
||||||
pub token: String,
|
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 {
|
pub enum CURegState {
|
||||||
// Nothing in progress.
|
// Nothing in progress.
|
||||||
None,
|
None,
|
||||||
|
@ -1181,14 +1182,14 @@ pub enum CURegState {
|
||||||
Passkey(CreationChallengeResponse),
|
Passkey(CreationChallengeResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub enum CUExtPortal {
|
pub enum CUExtPortal {
|
||||||
None,
|
None,
|
||||||
Hidden,
|
Hidden,
|
||||||
Some(Url),
|
Some(Url),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct CUStatus {
|
pub struct CUStatus {
|
||||||
// Display values
|
// Display values
|
||||||
pub spn: String,
|
pub spn: String,
|
||||||
|
@ -1204,7 +1205,7 @@ pub struct CUStatus {
|
||||||
pub passkeys_can_edit: bool,
|
pub passkeys_can_edit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
|
||||||
pub struct WhoamiResponse {
|
pub struct WhoamiResponse {
|
||||||
// Should we just embed the entry? Or destructure it?
|
// Should we just embed the entry? Or destructure it?
|
||||||
pub youare: Entry,
|
pub youare: Entry,
|
||||||
|
@ -1217,7 +1218,7 @@ impl WhoamiResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple string value provision.
|
// Simple string value provision.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct SingleStringRequest {
|
pub struct SingleStringRequest {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ filetime = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
|
|
||||||
hyper = { workspace = true }
|
hyper = { workspace = true }
|
||||||
kanidm_proto = { workspace = true }
|
kanidm_proto = { workspace = true }
|
||||||
kanidm_utils_users = { workspace = true }
|
kanidm_utils_users = { workspace = true }
|
||||||
|
@ -63,6 +62,15 @@ urlencoding = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
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]
|
[build-dependencies]
|
||||||
kanidm_build_profiles = { workspace = true }
|
kanidm_build_profiles = { workspace = true }
|
||||||
|
|
|
@ -308,6 +308,12 @@ impl fmt::Display for Configuration {
|
||||||
|
|
||||||
impl Default for Configuration {
|
impl Default for Configuration {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn new() -> Self {
|
||||||
Configuration {
|
Configuration {
|
||||||
address: DEFAULT_SERVER_ADDRESS.to_string(),
|
address: DEFAULT_SERVER_ADDRESS.to_string(),
|
||||||
ldapaddress: None,
|
ldapaddress: None,
|
||||||
|
@ -335,12 +341,6 @@ impl Default for Configuration {
|
||||||
integration_repl_config: None,
|
integration_repl_config: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Configuration {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_for_test() -> Self {
|
pub fn new_for_test() -> Self {
|
||||||
Configuration {
|
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},
|
http::{header::HeaderName, request::Parts, StatusCode},
|
||||||
RequestPartsExt,
|
RequestPartsExt,
|
||||||
};
|
};
|
||||||
|
use kanidm_proto::constants::X_FORWARDED_FOR;
|
||||||
|
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
use crate::https::ServerState;
|
use crate::https::ServerState;
|
||||||
|
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[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);
|
pub struct TrustedClientIp(pub IpAddr);
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ impl FromRequestParts<ServerState> for TrustedClientIp {
|
||||||
state: &ServerState,
|
state: &ServerState,
|
||||||
) -> Result<Self, Self::Rejection> {
|
) -> Result<Self, Self::Rejection> {
|
||||||
if state.trust_x_forward_for {
|
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.
|
// X forward for may be comma separate.
|
||||||
let first = x_forward_for
|
let first = x_forward_for
|
||||||
.to_str()
|
.to_str()
|
||||||
|
|
|
@ -1,26 +1,45 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
use axum::routing::get;
|
||||||
|
use axum::{Extension, Router};
|
||||||
use http::header::CONTENT_TYPE;
|
use http::header::CONTENT_TYPE;
|
||||||
use kanidmd_lib::status::StatusRequestEvent;
|
use kanidmd_lib::status::StatusRequestEvent;
|
||||||
|
|
||||||
use super::middleware::KOpId;
|
use super::middleware::KOpId;
|
||||||
use super::ServerState;
|
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(
|
pub async fn status(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> String {
|
||||||
let r = state
|
let r = state
|
||||||
.status_ref
|
.status_ref
|
||||||
.handle_request(StatusRequestEvent {
|
.handle_request(StatusRequestEvent {
|
||||||
eventid: kopid.eventid,
|
eventid: kopid.eventid,
|
||||||
})
|
})
|
||||||
.await;
|
.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 {
|
pub async fn robots_txt() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[(CONTENT_TYPE, "text/plain;charset=utf-8")],
|
[(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::{
|
use axum::{
|
||||||
|
headers::{CacheControl, HeaderMapExt},
|
||||||
http::{self, Request},
|
http::{self, Request},
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::Response,
|
response::Response,
|
||||||
|
@ -18,3 +19,19 @@ pub async fn dont_cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
|
||||||
|
|
||||||
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,
|
TypedHeader,
|
||||||
};
|
};
|
||||||
use http::HeaderValue;
|
use http::HeaderValue;
|
||||||
|
use kanidm_proto::constants::{KOPID, KVERSION};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(crate) mod caching;
|
pub(crate) mod caching;
|
||||||
pub(crate) mod compression;
|
pub(crate) mod compression;
|
||||||
pub(crate) mod hsts_header;
|
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;
|
let mut response = next.run(request).await;
|
||||||
response
|
response
|
||||||
.headers_mut()
|
.headers_mut()
|
||||||
.insert("X-KANIDM-VERSION", HeaderValue::from_static(KANIDM_VERSION));
|
.insert(KVERSION, HeaderValue::from_static(KANIDM_VERSION));
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
/// For holding onto the event ID and other handy request-based things
|
/// For holding onto the event ID and other handy request-based things
|
||||||
pub struct KOpId {
|
pub struct KOpId {
|
||||||
|
/// The event correlation ID
|
||||||
pub eventid: Uuid,
|
pub eventid: Uuid,
|
||||||
|
/// The User Access Token, if present
|
||||||
pub uat: Option<String>,
|
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.contains_key(http::header::CONTENT_TYPE));
|
||||||
assert!(
|
assert!(
|
||||||
headers.get(http::header::CONTENT_TYPE)
|
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
|
// This conversion *should never* fail. If it does, rather than panic, we warn and
|
||||||
// just don't put the id in the response.
|
// just don't put the id in the response.
|
||||||
let _ = HeaderValue::from_str(&eventid.as_hyphenated().to_string())
|
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| {
|
.map_err(|err| {
|
||||||
warn!(?err, "An invalid operation id was encountered");
|
warn!(?err, "An invalid operation id was encountered");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
mod apidocs;
|
||||||
|
pub(crate) mod errors;
|
||||||
|
|
||||||
mod extractors;
|
mod extractors;
|
||||||
mod generic;
|
mod generic;
|
||||||
mod javascript;
|
mod javascript;
|
||||||
|
@ -8,35 +11,31 @@ mod tests;
|
||||||
pub(crate) mod trace;
|
pub(crate) mod trace;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod v1;
|
mod v1;
|
||||||
|
mod v1_oauth2;
|
||||||
mod v1_scim;
|
mod v1_scim;
|
||||||
|
|
||||||
use self::generic::*;
|
|
||||||
use self::javascript::*;
|
use self::javascript::*;
|
||||||
use crate::actors::v1_read::QueryServerReadV1;
|
use crate::actors::v1_read::QueryServerReadV1;
|
||||||
use crate::actors::v1_write::QueryServerWriteV1;
|
use crate::actors::v1_write::QueryServerWriteV1;
|
||||||
use crate::config::{Configuration, ServerRole, TlsConfiguration};
|
use crate::config::{Configuration, ServerRole, TlsConfiguration};
|
||||||
use axum::extract::connect_info::{IntoMakeServiceWithConnectInfo, ResponseFuture};
|
use axum::extract::connect_info::{IntoMakeServiceWithConnectInfo, ResponseFuture};
|
||||||
use axum::middleware::{from_fn, from_fn_with_state};
|
use axum::middleware::{from_fn, from_fn_with_state};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::Redirect;
|
||||||
use axum::routing::*;
|
use axum::routing::*;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum_csp::{CspDirectiveType, CspValue};
|
use axum_csp::{CspDirectiveType, CspValue};
|
||||||
use axum_macros::FromRef;
|
use axum_macros::FromRef;
|
||||||
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
|
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
|
||||||
use http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE};
|
use http::{HeaderMap, HeaderValue};
|
||||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
|
||||||
use hyper::server::accept::Accept;
|
use hyper::server::accept::Accept;
|
||||||
use hyper::server::conn::{AddrStream, Http};
|
use hyper::server::conn::{AddrStream, Http};
|
||||||
use hyper::Body;
|
use kanidm_proto::constants::KSESSIONID;
|
||||||
use kanidm_proto::constants::APPLICATION_JSON;
|
|
||||||
use kanidm_proto::v1::OperationError;
|
|
||||||
use kanidmd_lib::status::StatusActor;
|
use kanidmd_lib::status::StatusActor;
|
||||||
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod};
|
use openssl::ssl::{Ssl, SslAcceptor, SslFiletype, SslMethod};
|
||||||
use sketching::*;
|
use sketching::*;
|
||||||
use tokio_openssl::SslStream;
|
use tokio_openssl::SslStream;
|
||||||
|
|
||||||
use futures_util::future::poll_fn;
|
use futures_util::future::poll_fn;
|
||||||
use serde::Serialize;
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
|
|
||||||
|
@ -82,7 +81,7 @@ impl ServerState {
|
||||||
fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option<Uuid> {
|
fn get_current_auth_session_id(&self, headers: &HeaderMap) -> Option<Uuid> {
|
||||||
// We see if there is a signed header copy first.
|
// We see if there is a signed header copy first.
|
||||||
headers
|
headers
|
||||||
.get("X-KANIDM-AUTH-SESSION-ID")
|
.get(KSESSIONID)
|
||||||
.and_then(|hv| {
|
.and_then(|hv| {
|
||||||
// Get the first header value.
|
// Get the first header value.
|
||||||
hv.to_str().ok()
|
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's set up the list of js module hashes
|
||||||
{
|
{
|
||||||
let filepath = "wasmloader.js";
|
let filepath = "wasmloader.js";
|
||||||
#[allow(clippy::unwrap_used)]
|
match generate_integrity_hash(format!(
|
||||||
js_files.push(JavaScriptFile {
|
"{}/{}",
|
||||||
|
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||||
filepath,
|
filepath,
|
||||||
hash: generate_integrity_hash(format!(
|
)) {
|
||||||
"{}/{}",
|
Ok(hash) => js_files.push(JavaScriptFile {
|
||||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
|
||||||
filepath,
|
filepath,
|
||||||
))
|
hash,
|
||||||
.unwrap(),
|
filetype: Some("module".to_string()),
|
||||||
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's set up the list of non-module hashes
|
||||||
{
|
{
|
||||||
let filepath = "external/bootstrap.bundle.min.js";
|
let filepath = "external/bootstrap.bundle.min.js";
|
||||||
#[allow(clippy::unwrap_used)]
|
match generate_integrity_hash(format!(
|
||||||
js_files.push(JavaScriptFile {
|
"{}/{}",
|
||||||
|
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||||
filepath,
|
filepath,
|
||||||
hash: generate_integrity_hash(format!(
|
)) {
|
||||||
"{}/{}",
|
Ok(hash) =>
|
||||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
js_files.push(JavaScriptFile {
|
||||||
filepath,
|
filepath,
|
||||||
))
|
hash,
|
||||||
.unwrap(),
|
filetype: None,
|
||||||
filetype: None,
|
}),
|
||||||
});
|
Err(err) => {
|
||||||
|
admin_error!(?err, "Failed to generate integrity hash for bootstrap.bundle.min.js")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
js_files
|
js_files
|
||||||
|
@ -197,28 +203,25 @@ pub async fn create_https_server(
|
||||||
let static_routes = match config.role {
|
let static_routes = match config.role {
|
||||||
ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => {
|
ServerRole::WriteReplica | ServerRole::ReadOnlyReplica => {
|
||||||
// Create a spa router that captures everything at ui without key extraction.
|
// 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()
|
Router::new()
|
||||||
// direct users to the base app page. If a login is required,
|
// direct users to the base app page. If a login is required,
|
||||||
// then views will take care of redirection. We shouldn't redir
|
// then views will take care of redirection. We shouldn't redir
|
||||||
// to login because that force clears previous sessions!
|
// to login because that force clears previous sessions!
|
||||||
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
||||||
.route("/manifest.webmanifest", get(manifest::manifest))
|
.route("/manifest.webmanifest", get(manifest::manifest)) // skip_route_check
|
||||||
.nest("/ui", spa_router)
|
.nest("/ui", ui::spa_router())
|
||||||
.layer(middleware::compression::new())
|
.layer(middleware::compression::new())
|
||||||
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||||
|
// skip_route_check
|
||||||
}
|
}
|
||||||
ServerRole::WriteReplicaNoUI => Router::new(),
|
ServerRole::WriteReplicaNoUI => Router::new(),
|
||||||
};
|
};
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/robots.txt", get(robots_txt))
|
.merge(generic::route_setup())
|
||||||
.route("/status", get(status))
|
.merge(oauth2::route_setup(state.clone()))
|
||||||
.merge(oauth2::oauth2_route_setup(state.clone()))
|
.merge(v1_scim::route_setup())
|
||||||
.merge(v1_scim::scim_route_setup())
|
.merge(v1::route_setup(state.clone()));
|
||||||
.merge(v1::router(state.clone()));
|
|
||||||
|
|
||||||
let app = match config.role {
|
let app = match config.role {
|
||||||
ServerRole::WriteReplicaNoUI => app,
|
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
|
// to be exited, and this middleware sets up ids' and other bits for for logging
|
||||||
// coherence to be maintained.
|
// coherence to be maintained.
|
||||||
.layer(from_fn(middleware::kopid_middleware))
|
.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.
|
// this MUST be the last layer before with_state else the span never starts and everything breaks.
|
||||||
.layer(trace_layer)
|
.layer(trace_layer)
|
||||||
.with_state(state)
|
.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::middleware::KOpId;
|
||||||
use super::v1::{json_rest_event_get, json_rest_event_post};
|
use super::ServerState;
|
||||||
use super::{to_axum_response, HttpOperationError, ServerState};
|
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
use axum::middleware::from_fn;
|
use axum::middleware::from_fn;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Extension, Form, Json, Router};
|
use axum::{Extension, Form, Json, Router};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
|
use compact_jwt::{JwkKeySet, OidcToken};
|
||||||
use http::header::{
|
use http::header::{
|
||||||
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION, CONTENT_TYPE,
|
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, AUTHORIZATION, CONTENT_TYPE,
|
||||||
LOCATION, WWW_AUTHENTICATE,
|
LOCATION, WWW_AUTHENTICATE,
|
||||||
|
@ -14,9 +15,7 @@ use http::header::{
|
||||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||||
use hyper::Body;
|
use hyper::Body;
|
||||||
use kanidm_proto::constants::APPLICATION_JSON;
|
use kanidm_proto::constants::APPLICATION_JSON;
|
||||||
use kanidm_proto::internal::{ImageType, ImageValue};
|
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse, AccessTokenResponse};
|
||||||
use kanidm_proto::oauth2::{AuthorisationResponse, OidcDiscoveryResponse};
|
|
||||||
use kanidm_proto::v1::Entry as ProtoEntry;
|
|
||||||
use kanidmd_lib::idm::oauth2::{
|
use kanidmd_lib::idm::oauth2::{
|
||||||
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
|
||||||
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
|
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
|
||||||
|
@ -24,9 +23,9 @@ use kanidmd_lib::idm::oauth2::{
|
||||||
use kanidmd_lib::prelude::f_eq;
|
use kanidmd_lib::prelude::f_eq;
|
||||||
use kanidmd_lib::prelude::*;
|
use kanidmd_lib::prelude::*;
|
||||||
use kanidmd_lib::value::PartialValue;
|
use kanidmd_lib::value::PartialValue;
|
||||||
use kanidmd_lib::valueset::image::ImageValueThings;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// TODO: merge this into a value in WebError later
|
||||||
pub struct HTTPOauth2Error(Oauth2Error);
|
pub struct HTTPOauth2Error(Oauth2Error);
|
||||||
|
|
||||||
impl IntoResponse for HTTPOauth2Error {
|
impl IntoResponse for HTTPOauth2Error {
|
||||||
|
@ -34,17 +33,18 @@ impl IntoResponse for HTTPOauth2Error {
|
||||||
let HTTPOauth2Error(error) = self;
|
let HTTPOauth2Error(error) = self;
|
||||||
|
|
||||||
if let Oauth2Error::AuthenticationRequired = error {
|
if let Oauth2Error::AuthenticationRequired = error {
|
||||||
#[allow(clippy::unwrap_used)]
|
(
|
||||||
Response::builder()
|
StatusCode::UNAUTHORIZED,
|
||||||
.status(StatusCode::UNAUTHORIZED)
|
[
|
||||||
.header(WWW_AUTHENTICATE, "Bearer")
|
(WWW_AUTHENTICATE, "Bearer"),
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
|
||||||
.body(Body::empty())
|
],
|
||||||
.unwrap()
|
)
|
||||||
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
let err = ErrorResponse {
|
let err = ErrorResponse {
|
||||||
error: error.to_string(),
|
error: error.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = match serde_json::to_string(&err) {
|
let body = match serde_json::to_string(&err) {
|
||||||
|
@ -54,299 +54,71 @@ impl IntoResponse for HTTPOauth2Error {
|
||||||
format!("{:?}", err)
|
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()
|
||||||
}
|
}
|
||||||
.into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// == Oauth2 Configuration Endpoints ==
|
// == 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
|
/// 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!([
|
filter_all!(f_and!([
|
||||||
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
|
||||||
f_eq(Attribute::OAuth2RsName, PartialValue::new_iname(rs_name))
|
f_eq(Attribute::OAuth2RsName, PartialValue::new_iname(rs_name))
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn oauth2_id_get(
|
#[utoipa::path(
|
||||||
State(state): State<ServerState>,
|
get,
|
||||||
Path(rs_name): Path<String>,
|
path = "/ui/images/oauth2/{rs_name}",
|
||||||
Extension(kopid): Extension<KOpId>,
|
params(
|
||||||
) -> Response<Body> {
|
super::apidocs::path_schema::RsName
|
||||||
let filter = oauth2_id(&rs_name);
|
),
|
||||||
|
responses(
|
||||||
let res = state
|
(status = 200, description = "Ok", body=&[u8]),
|
||||||
.qe_r_ref
|
(status = 403, description = "Authorization refused"),
|
||||||
.handle_internalsearch(kopid.uat, filter, None, kopid.eventid)
|
(status = 403, description = "Authorization refused"),
|
||||||
.await
|
),
|
||||||
.map(|mut r| r.pop());
|
security(("token_jwt" = [])),
|
||||||
to_axum_response(res)
|
tag = "ui",
|
||||||
}
|
)]
|
||||||
|
/// This returns the image for the OAuth2 Resource Server if the user has permissions
|
||||||
#[instrument(level = "info", skip(state))]
|
///
|
||||||
pub async fn oauth2_id_get_basic_secret(
|
pub(crate) async fn oauth2_image_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Path(rs_name): Path<String>,
|
Path(rs_name): Path<String>,
|
||||||
) -> Response<Body> {
|
) -> Response {
|
||||||
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> {
|
|
||||||
let rs_filter = oauth2_id(&rs_name);
|
let rs_filter = oauth2_id(&rs_name);
|
||||||
let res = state
|
let res = state
|
||||||
.qe_r_ref
|
.qe_r_ref
|
||||||
.handle_oauth2_rs_image_get_image(kopid.uat, rs_filter)
|
.handle_oauth2_rs_image_get_image(kopid.uat, rs_filter)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let image = match res {
|
match res {
|
||||||
Ok(image) => image,
|
Ok(image) => (
|
||||||
Err(_err) => {
|
StatusCode::OK,
|
||||||
admin_error!(
|
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
|
||||||
"Unable to get image for oauth2 resource server: {}",
|
image.contents,
|
||||||
rs_name
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(err) => {
|
||||||
|
admin_debug!(
|
||||||
|
"Unable to get image for oauth2 resource server {}: {:?}",
|
||||||
|
rs_name,
|
||||||
|
err
|
||||||
);
|
);
|
||||||
#[allow(clippy::unwrap_used)]
|
// TODO: a 404 probably isn't perfect but it's not the worst
|
||||||
return Response::builder()
|
(StatusCode::NOT_FOUND, "").into_response()
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 ==
|
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
|
||||||
|
@ -701,7 +473,7 @@ pub async fn oauth2_token_post(
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Form(tok_req): Form<AccessTokenRequest>,
|
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
|
// This is called directly by the resource server, where we then issue
|
||||||
// the token to the caller.
|
// the token to the caller.
|
||||||
|
|
||||||
|
@ -731,7 +503,7 @@ pub async fn oauth2_openid_discovery_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(client_id): Path<String>,
|
Path(client_id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> Result<Json<OidcDiscoveryResponse>, HttpOperationError> {
|
) -> Result<Json<OidcDiscoveryResponse>, Response> {
|
||||||
// let client_id = req.get_url_param("client_id")?;
|
// let client_id = req.get_url_param("client_id")?;
|
||||||
|
|
||||||
let res = state
|
let res = state
|
||||||
|
@ -743,7 +515,7 @@ pub async fn oauth2_openid_discovery_get(
|
||||||
Ok(dsc) => Ok(Json(dsc)),
|
Ok(dsc) => Ok(Json(dsc)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(err = ?e, "Unable to access discovery info");
|
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>,
|
State(state): State<ServerState>,
|
||||||
Path(client_id): Path<String>,
|
Path(client_id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<OidcToken>, HTTPOauth2Error> {
|
||||||
// The token we want to inspect is in the authorisation header.
|
// The token we want to inspect is in the authorisation header.
|
||||||
let client_token = match kopid.uat {
|
let client_token = match kopid.uat {
|
||||||
Some(val) => val,
|
Some(val) => val,
|
||||||
|
@ -778,13 +550,13 @@ pub async fn oauth2_openid_publickey_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(client_id): Path<String>,
|
Path(client_id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> Response<Body> {
|
) -> Result<Json<JwkKeySet>, WebError> {
|
||||||
to_axum_response(
|
state
|
||||||
state
|
.qe_r_ref
|
||||||
.qe_r_ref
|
.handle_oauth2_openid_publickey(client_id, kopid.eventid)
|
||||||
.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
|
/// 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,
|
Some(val) => val,
|
||||||
None =>
|
None =>
|
||||||
{
|
{
|
||||||
#[allow(clippy::unwrap_used)]
|
return (
|
||||||
return Response::builder()
|
StatusCode::UNAUTHORIZED,
|
||||||
.status(StatusCode::UNAUTHORIZED)
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
""
|
||||||
.body(Body::empty())
|
)
|
||||||
.unwrap()
|
.into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -906,21 +678,17 @@ pub async fn oauth2_token_revoke_post(
|
||||||
match res {
|
match res {
|
||||||
Ok(()) =>
|
Ok(()) =>
|
||||||
{
|
{
|
||||||
#[allow(clippy::unwrap_used)]
|
(StatusCode::OK,
|
||||||
Response::builder()
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||||
.status(StatusCode::OK)
|
""
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
).into_response()
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
Err(Oauth2Error::AuthenticationRequired) => {
|
Err(Oauth2Error::AuthenticationRequired) => {
|
||||||
// This will trigger our ui to auth and retry.
|
// This will trigger our ui to auth and retry.
|
||||||
#[allow(clippy::unwrap_used)]
|
(StatusCode::UNAUTHORIZED,
|
||||||
Response::builder()
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),],
|
||||||
.status(StatusCode::UNAUTHORIZED)
|
""
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
).into_response()
|
||||||
.body(Body::empty())
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
// 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(),
|
error: e.to_string(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
#[allow(clippy::unwrap_used)]
|
(StatusCode::BAD_REQUEST,
|
||||||
Response::builder()
|
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),],
|
||||||
.status(StatusCode::BAD_REQUEST)
|
serde_json::to_string(&err).unwrap_or("".to_string()),
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
).into_response()
|
||||||
.body(Body::from(
|
|
||||||
serde_json::to_string(&err).unwrap_or("".to_string()),
|
|
||||||
))
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some requests from browsers require preflight so that CORS works.
|
// Some requests from browsers require preflight so that CORS works.
|
||||||
pub async fn oauth2_preflight_options() -> Response<Body> {
|
pub async fn oauth2_preflight_options() -> Response {
|
||||||
#[allow(clippy::unwrap_used)]
|
(
|
||||||
Response::builder()
|
StatusCode::OK,
|
||||||
.status(StatusCode::OK)
|
[
|
||||||
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
|
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
|
||||||
.header(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization")
|
(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
|
||||||
.body(Body::empty())
|
],
|
||||||
.unwrap()
|
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
|
// this has all the openid-related routes
|
||||||
let openid_router = Router::new()
|
let openid_router = Router::new()
|
||||||
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||||
|
@ -975,7 +740,7 @@ pub fn oauth2_route_setup(state: ServerState) -> Router<ServerState> {
|
||||||
.with_state(state.clone());
|
.with_state(state.clone());
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/oauth2", get(oauth2_get))
|
.route("/oauth2", get(super::v1_oauth2::oauth2_get))
|
||||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||||
.route(
|
.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::extract::State;
|
||||||
use axum::http::HeaderValue;
|
use axum::http::HeaderValue;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::Extension;
|
use axum::routing::get;
|
||||||
|
use axum::{Extension, Router};
|
||||||
use http::header::CONTENT_TYPE;
|
use http::header::CONTENT_TYPE;
|
||||||
|
|
||||||
use super::middleware::KOpId;
|
use super::middleware::KOpId;
|
||||||
use super::ServerState;
|
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>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> Response<String> {
|
) -> 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::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::{
|
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_get, json_rest_event_get_id, json_rest_event_get_id_attr, json_rest_event_post,
|
||||||
json_rest_event_put_id_attr,
|
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(
|
pub async fn sync_account_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<Vec<ProtoEntry>>, WebError> {
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
json_rest_event_get(state, None, filter, kopid).await
|
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(
|
pub async fn sync_account_post(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Json(obj): Json<ProtoEntry>,
|
Json(obj): Json<ProtoEntry>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let classes: Vec<String> = vec![EntryClass::SyncAccount.into(), EntryClass::Object.into()];
|
let classes: Vec<String> = vec![EntryClass::SyncAccount.into(), EntryClass::Object.into()];
|
||||||
json_rest_event_post(state, classes, obj, kopid).await
|
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(
|
pub async fn sync_account_id_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<Option<ProtoEntry>>, WebError> {
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
json_rest_event_get_id(state, id, filter, None, kopid).await
|
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(
|
pub async fn sync_account_id_patch(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Json(obj): Json<ProtoEntry>,
|
Json(obj): Json<ProtoEntry>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
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 filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
|
||||||
|
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
|
.handle_internalpatch(kopid.uat, filter, obj, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sync_account_finalise(kopid.uat, id, kopid.eventid)
|
.handle_sync_account_finalise(kopid.uat, id, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sync_account_terminate(kopid.uat, id, kopid.eventid)
|
.handle_sync_account_terminate(kopid.uat, id, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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(
|
pub async fn sync_account_token_post(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Json(label): Json<String>,
|
Json(label): Json<String>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<String>, WebError> {
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sync_account_token_generate(kopid.uat, id, label, kopid.eventid)
|
.handle_sync_account_token_generate(kopid.uat, id, label, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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(
|
pub async fn sync_account_token_delete(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_sync_account_token_destroy(kopid.uat, id, kopid.eventid)
|
.handle_sync_account_token_destroy(kopid.uat, id, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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(
|
async fn scim_sync_post(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
AuthBearer(bearer): AuthBearer,
|
AuthBearer(bearer): AuthBearer,
|
||||||
Json(changes): Json<ScimSyncRequest>,
|
Json(changes): Json<ScimSyncRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let res = state
|
state
|
||||||
.qe_w_ref
|
.qe_w_ref
|
||||||
.handle_scim_sync_apply(Some(bearer), changes, kopid.eventid)
|
.handle_scim_sync_apply(Some(bearer), changes, kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.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(
|
async fn scim_sync_get(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
AuthBearer(bearer): AuthBearer,
|
AuthBearer(bearer): AuthBearer,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<ScimSyncState>, WebError> {
|
||||||
// Given the token, what is it's connected sync state?
|
// Given the token, what is it's connected sync state?
|
||||||
trace!(?bearer);
|
trace!(?bearer);
|
||||||
let res = state
|
state
|
||||||
.qe_r_ref
|
.qe_r_ref
|
||||||
.handle_scim_sync_status(Some(bearer), kopid.eventid)
|
.handle_scim_sync_status(Some(bearer), kopid.eventid)
|
||||||
.await;
|
.await
|
||||||
to_axum_response(res)
|
.map(Json::from)
|
||||||
|
.map_err(WebError::from)
|
||||||
}
|
}
|
||||||
|
#[utoipa::path(
|
||||||
pub async fn sync_account_id_get_attr(
|
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>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Path((id, attr)): Path<(String, String)>,
|
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()));
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
json_rest_event_get_id_attr(state, id, attr, filter, kopid).await
|
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>,
|
State(state): State<ServerState>,
|
||||||
Extension(kopid): Extension<KOpId>,
|
Extension(kopid): Extension<KOpId>,
|
||||||
Path((id, attr)): Path<(String, String)>,
|
Path((id, attr)): Path<(String, String)>,
|
||||||
Json(values): Json<Vec<String>>,
|
Json(values): Json<Vec<String>>,
|
||||||
) -> impl IntoResponse {
|
) -> Result<Json<()>, WebError> {
|
||||||
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
let filter = filter_all!(f_eq(Attribute::Class, EntryClass::SyncAccount.into()));
|
||||||
json_rest_event_put_id_attr(state, id, attr, filter, values, kopid).await
|
json_rest_event_put_id_attr(state, id, attr, filter, values, kopid).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scim_sink_get() -> impl IntoResponse {
|
/// When you want the kitchen Sink
|
||||||
r#"
|
async fn scim_sink_get() -> Html<&'static str> {
|
||||||
<!DOCTYPE html>
|
Html::from(include_str!("scim/sink.html"))
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="theme-color" content="white" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<title>Sink!</title>
|
|
||||||
<link rel="icon" href="/pkg/img/favicon.png" />
|
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<pre>
|
|
||||||
___
|
|
||||||
.' _ '.
|
|
||||||
/ /` `\ \
|
|
||||||
| | [__]
|
|
||||||
| | {{
|
|
||||||
| | }}
|
|
||||||
_ | | _ {{
|
|
||||||
___________<_>_| |_<_>}}________
|
|
||||||
.=======^=(___)=^={{====.
|
|
||||||
/ .----------------}}---. \
|
|
||||||
/ / {{ \ \
|
|
||||||
/ / }} \ \
|
|
||||||
( '=========================' )
|
|
||||||
'-----------------------------'
|
|
||||||
</pre>
|
|
||||||
</body>
|
|
||||||
</html>"#
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scim_route_setup() -> Router<ServerState> {
|
pub fn route_setup() -> Router<ServerState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
// https://datatracker.ietf.org/doc/html/rfc7644#section-3.2
|
// 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
|
// 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/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,
|
replica_connect_timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn repl_task(
|
async fn repl_task(
|
||||||
origin: Url,
|
origin: Url,
|
||||||
client_key: PKey<Private>,
|
client_key: PKey<Private>,
|
||||||
|
@ -827,7 +828,7 @@ async fn repl_acceptor(
|
||||||
}
|
}
|
||||||
|
|
||||||
if respond.send(success).is_err() {
|
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 {
|
} else {
|
||||||
trace!("Sent server certificate renewal status via control channel");
|
trace!("Sent server certificate renewal status via control channel");
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,11 +278,13 @@ impl fmt::Debug for FilterResolved {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
/// A filter before it's gone through schema validation
|
||||||
pub struct FilterInvalid {
|
pub struct FilterInvalid {
|
||||||
inner: FilterComp,
|
inner: FilterComp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
/// A filter after it's gone through schema validation
|
||||||
pub struct FilterValid {
|
pub struct FilterValid {
|
||||||
inner: FilterComp,
|
inner: FilterComp,
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ kanidmd_core = { workspace = true }
|
||||||
kanidmd_lib = { workspace = true }
|
kanidmd_lib = { workspace = true }
|
||||||
# used for webdriver testing
|
# used for webdriver testing
|
||||||
hyper-tls = { workspace = true }
|
hyper-tls = { workspace = true }
|
||||||
|
http = { workspace = true }
|
||||||
# used for webdriver testing
|
# used for webdriver testing
|
||||||
fantoccini = { version = "0.19.3", optional = true }
|
fantoccini = { version = "0.19.3", optional = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
@ -649,31 +649,19 @@ async fn test_https_robots_txt(rsclient: KanidmClient) {
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"csp headers: {:#?}",
|
"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());
|
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...
|
/// This literally tests that the thing exists and responds in a way we expect, probably worth testing it better...
|
||||||
#[kanidmd_testkit::test]
|
#[kanidmd_testkit::test]
|
||||||
async fn test_v1_raw_delete(rsclient: KanidmClient) {
|
async fn test_v1_raw_delete(rsclient: KanidmClient) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ async fn test_https_manifest(rsclient: KanidmClient) {
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"csp headers: {:#?}",
|
"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_client::KanidmClient;
|
||||||
|
use kanidm_proto::constants::X_FORWARDED_FOR;
|
||||||
|
|
||||||
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
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
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header(
|
.header(
|
||||||
"X-Forwarded-For",
|
X_FORWARDED_FOR,
|
||||||
"An invalid header that will get through!!!",
|
"An invalid header that will get through!!!",
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
|
@ -41,7 +42,7 @@ async fn dont_trust_xff_dont_send_header(rsclient: KanidmClient) {
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header(
|
.header(
|
||||||
"X-Forwarded-For",
|
X_FORWARDED_FOR,
|
||||||
"An invalid header that will get through!!!",
|
"An invalid header that will get through!!!",
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
|
@ -69,7 +70,7 @@ async fn trust_xff_send_invalid_header_single_value(rsclient: KanidmClient) {
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header(
|
.header(
|
||||||
"X-Forwarded-For",
|
X_FORWARDED_FOR,
|
||||||
"An invalid header that will get through!!!",
|
"An invalid header that will get through!!!",
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
|
@ -91,7 +92,7 @@ async fn trust_xff_send_invalid_header_multiple_values(rsclient: KanidmClient) {
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header(
|
.header(
|
||||||
"X-Forwarded-For",
|
X_FORWARDED_FOR,
|
||||||
"203.0.113.195_noooo_my_ip_address, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
"203.0.113.195_noooo_my_ip_address, 2001:db8:85a3:8d3:1319:8a2e:370:7348",
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
|
@ -111,7 +112,7 @@ async fn trust_xff_send_valid_header_single_ipv4_address(rsclient: KanidmClient)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header("X-Forwarded-For", ip_addr)
|
.header(X_FORWARDED_FOR, ip_addr)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -133,7 +134,7 @@ async fn trust_xff_send_valid_header_single_ipv6_address(rsclient: KanidmClient)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header("X-Forwarded-For", ip_addr)
|
.header(X_FORWARDED_FOR, ip_addr)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -155,7 +156,7 @@ async fn trust_xff_send_valid_header_multiple_address(rsclient: KanidmClient) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header("X-Forwarded-For", first_ip_addr)
|
.header(X_FORWARDED_FOR, first_ip_addr)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -177,7 +178,7 @@ async fn trust_xff_send_valid_header_multiple_address(rsclient: KanidmClient) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let res = client
|
let res = client
|
||||||
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
.get(rsclient.make_url("/v1/debug/ipinfo"))
|
||||||
.header("X-Forwarded-For", second_ip_addr)
|
.header(X_FORWARDED_FOR, second_ip_addr)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -19,9 +19,16 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
|
||||||
assert_eq!(response.status(), 200);
|
assert_eq!(response.status(), 200);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"csp headers: {:#?}",
|
"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
|
// here we test the /ui/login endpoint which should have the headers
|
||||||
let response = match reqwest::get(rsclient.make_url("/ui/login")).await {
|
let response = match reqwest::get(rsclient.make_url("/ui/login")).await {
|
||||||
|
@ -39,7 +46,14 @@ async fn test_https_middleware_headers(rsclient: KanidmClient) {
|
||||||
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"csp headers: {:#?}",
|
"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.
|
// Check we have correct nocache headers.
|
||||||
let cache_header: &str = $response
|
let cache_header: &str = $response
|
||||||
.headers()
|
.headers()
|
||||||
.get("cache-control")
|
.get(http::header::CACHE_CONTROL)
|
||||||
.expect("missing cache-control header")
|
.expect("missing cache-control header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("invalid cache-control header");
|
.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.");
|
.expect("Failed to send discovery preflight request.");
|
||||||
|
|
||||||
assert!(response.status() == reqwest::StatusCode::OK);
|
assert!(response.status() == reqwest::StatusCode::OK);
|
||||||
|
|
||||||
let cors_header: &str = response
|
let cors_header: &str = response
|
||||||
.headers()
|
.headers()
|
||||||
.get("access-control-allow-origin")
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
.expect("missing access-control-allow-origin header")
|
.expect("missing access-control-allow-origin header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("invalid access-control-allow-origin header");
|
.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);
|
assert!(response.status() == reqwest::StatusCode::OK);
|
||||||
let cors_header: &str = response
|
let cors_header: &str = response
|
||||||
.headers()
|
.headers()
|
||||||
.get("access-control-allow-origin")
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
.expect("missing access-control-allow-origin header")
|
.expect("missing access-control-allow-origin header")
|
||||||
.to_str()
|
.to_str()
|
||||||
.expect("invalid access-control-allow-origin header");
|
.expect("invalid access-control-allow-origin header");
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use kanidm_proto::constants::KSESSIONID;
|
||||||
use kanidm_proto::internal::ImageValue;
|
use kanidm_proto::internal::ImageValue;
|
||||||
use kanidm_proto::v1::{
|
use kanidm_proto::v1::{
|
||||||
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
ApiToken, AuthCredential, AuthIssueSession, AuthMech, AuthRequest, AuthResponse, AuthState,
|
||||||
|
@ -1826,7 +1827,7 @@ async fn start_password_session(
|
||||||
};
|
};
|
||||||
assert_eq!(res.status(), 200);
|
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 {
|
let authreq = AuthRequest {
|
||||||
step: AuthStep::Begin(AuthMech::Password),
|
step: AuthStep::Begin(AuthMech::Password),
|
||||||
|
@ -1836,7 +1837,7 @@ async fn start_password_session(
|
||||||
let res = match client
|
let res = match client
|
||||||
.post(rsclient.make_url("/v1/auth"))
|
.post(rsclient.make_url("/v1/auth"))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("x-kanidm-auth-session-id", session_id)
|
.header(KSESSIONID, session_id)
|
||||||
.body(authreq)
|
.body(authreq)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
@ -1854,7 +1855,7 @@ async fn start_password_session(
|
||||||
let res = match client
|
let res = match client
|
||||||
.post(rsclient.make_url("/v1/auth"))
|
.post(rsclient.make_url("/v1/auth"))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("x-kanidm-auth-session-id", session_id)
|
.header(KSESSIONID, session_id)
|
||||||
.body(authreq)
|
.body(authreq)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -105,6 +105,7 @@ async fn test_scim_sync_get(rsclient: KanidmClient) {
|
||||||
.default_headers(headers)
|
.default_headers(headers)
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// here we test the /ui/ endpoint which should have the headers
|
// here we test the /ui/ endpoint which should have the headers
|
||||||
let response = match client.get(rsclient.make_url("/scim/v1/Sync")).send().await {
|
let response = match client.get(rsclient.make_url("/scim/v1/Sync")).send().await {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
|
@ -117,12 +118,30 @@ async fn test_scim_sync_get(rsclient: KanidmClient) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
eprintln!("response: {:#?}", response);
|
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 error::FetchError;
|
||||||
use gloo::console;
|
use gloo::console;
|
||||||
use kanidm_proto::constants::APPLICATION_JSON;
|
use kanidm_proto::constants::{APPLICATION_JSON, KSESSIONID};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
@ -91,7 +91,7 @@ pub async fn do_request(
|
||||||
if let Some(sessionid) = models::pop_auth_session_id() {
|
if let Some(sessionid) = models::pop_auth_session_id() {
|
||||||
request
|
request
|
||||||
.headers()
|
.headers()
|
||||||
.set("x-kanidm-auth-session-id", &sessionid)
|
.set(KSESSIONID, &sessionid)
|
||||||
.expect_throw("failed to set auth session id header");
|
.expect_throw("failed to set auth session id header");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ pub async fn do_request(
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
let headers: Headers = resp.headers();
|
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);
|
models::push_auth_session_id(sessionid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue