mirror of
https://github.com/nitnelave/lldap.git
synced 2023-04-12 14:25:13 +00:00
Implement basic GraphQL endpoint with auth
This commit is contained in:
parent
be3e50d31a
commit
a51965a61a
172
Cargo.lock
generated
172
Cargo.lock
generated
@ -238,6 +238,23 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-actors"
|
||||||
|
version = "4.0.0-beta.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7db5c2c78a2606e6634abee4973a4924221cfab66e48f23844256e4fb8ce0f42"
|
||||||
|
dependencies = [
|
||||||
|
"actix",
|
||||||
|
"actix-codec",
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"bytes",
|
||||||
|
"bytestring",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "0.5.0-beta.3"
|
version = "0.5.0-beta.3"
|
||||||
@ -348,6 +365,12 @@ version = "0.5.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ascii"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_escape"
|
name = "askama_escape"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -491,6 +514,23 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bson"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "903a4f4c7aa97921f1703acac1fd524e9e082b3228edd34dde07758c0c92c672"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"lazy_static",
|
||||||
|
"linked-hash-map",
|
||||||
|
"rand 0.7.3",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "build_const"
|
name = "build_const"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -597,6 +637,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "3.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
|
||||||
|
dependencies = [
|
||||||
|
"ascii",
|
||||||
|
"byteorder",
|
||||||
|
"either",
|
||||||
|
"memchr",
|
||||||
|
"unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console_error_panic_hook"
|
name = "console_error_panic_hook"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@ -838,6 +891,17 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_utils"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "difference"
|
name = "difference"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -1006,6 +1070,17 @@ version = "0.3.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
|
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-enum"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3422d14de7903a52e9dbc10ae05a7e14445ec61890100e098754e120b2bd7b1e"
|
||||||
|
dependencies = [
|
||||||
|
"derive_utils",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -1167,6 +1242,16 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql-parser"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1abd4ce5247dfc04a03ccde70f87a048458c9356c7e41d21ad8c407b3dde6f2"
|
||||||
|
dependencies = [
|
||||||
|
"combine",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@ -1306,6 +1391,7 @@ checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg 1.0.1",
|
"autocfg 1.0.1",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1347,6 +1433,59 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "juniper"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "637ffa8a8d8a05aed3331449e311f145864adcd82442d82e54d0522decb7cecf"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bson",
|
||||||
|
"chrono",
|
||||||
|
"fnv",
|
||||||
|
"futures",
|
||||||
|
"futures-enum",
|
||||||
|
"graphql-parser",
|
||||||
|
"indexmap",
|
||||||
|
"juniper_codegen",
|
||||||
|
"serde",
|
||||||
|
"smartstring",
|
||||||
|
"static_assertions",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "juniper_actix"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc44af18ae1f551076171e24eb453c52132a19c219d1f1a1c3068ab363b946b5"
|
||||||
|
dependencies = [
|
||||||
|
"actix",
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"actix-web-actors",
|
||||||
|
"anyhow",
|
||||||
|
"futures",
|
||||||
|
"http",
|
||||||
|
"juniper",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "juniper_codegen"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a040e09482a45e77dd2dafa0d9d2651d17faf0ac674da0c93eabc3075ee24997"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jwt"
|
name = "jwt"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@ -1435,6 +1574,12 @@ dependencies = [
|
|||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linked-hash-map"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lldap"
|
name = "lldap"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -1460,6 +1605,8 @@ dependencies = [
|
|||||||
"futures-util",
|
"futures-util",
|
||||||
"hmac 0.10.1",
|
"hmac 0.10.1",
|
||||||
"http",
|
"http",
|
||||||
|
"juniper",
|
||||||
|
"juniper_actix",
|
||||||
"jwt",
|
"jwt",
|
||||||
"ldap3_server",
|
"ldap3_server",
|
||||||
"lldap_model",
|
"lldap_model",
|
||||||
@ -2400,6 +2547,7 @@ version = "1.0.64"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
@ -2490,6 +2638,15 @@ version = "1.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smartstring"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b"
|
||||||
|
dependencies = [
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -3097,6 +3254,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unreachable"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||||
|
dependencies = [
|
||||||
|
"void",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.2.2"
|
version = "2.2.2"
|
||||||
@ -3136,6 +3302,12 @@ version = "0.9.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "void"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.9.0+wasi-snapshot-preview1"
|
version = "0.9.0+wasi-snapshot-preview1"
|
||||||
|
@ -46,6 +46,8 @@ tracing-actix-web = "0.4.0-beta.7"
|
|||||||
tracing-log = "*"
|
tracing-log = "*"
|
||||||
tracing-subscriber = "*"
|
tracing-subscriber = "*"
|
||||||
rand = { version = "0.8", features = ["small_rng", "getrandom"] }
|
rand = { version = "0.8", features = ["small_rng", "getrandom"] }
|
||||||
|
juniper_actix = "0.4.0"
|
||||||
|
juniper = "0.15.6"
|
||||||
|
|
||||||
# TODO: update to 0.6 when out.
|
# TODO: update to 0.6 when out.
|
||||||
[dependencies.opaque-ke]
|
[dependencies.opaque-ke]
|
||||||
|
@ -337,6 +337,49 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ValidationResults {
|
||||||
|
pub user: String,
|
||||||
|
pub is_admin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationResults {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn admin() -> Self {
|
||||||
|
Self {
|
||||||
|
user: "admin".to_string(),
|
||||||
|
is_admin: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_access(&self, user: &str) -> bool {
|
||||||
|
self.is_admin || self.user == user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_if_token_is_valid<Backend>(
|
||||||
|
state: &AppState<Backend>,
|
||||||
|
token_str: &str,
|
||||||
|
) -> Result<ValidationResults, actix_web::Error> {
|
||||||
|
let token: Token<_> = VerifyWithKey::verify_with_key(token_str, &state.jwt_key)
|
||||||
|
.map_err(|_| ErrorUnauthorized("Invalid JWT"))?;
|
||||||
|
if token.claims().exp.lt(&Utc::now()) {
|
||||||
|
return Err(ErrorUnauthorized("Expired JWT"));
|
||||||
|
}
|
||||||
|
let jwt_hash = {
|
||||||
|
let mut s = DefaultHasher::new();
|
||||||
|
token_str.hash(&mut s);
|
||||||
|
s.finish()
|
||||||
|
};
|
||||||
|
if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) {
|
||||||
|
return Err(ErrorUnauthorized("JWT was logged out"));
|
||||||
|
}
|
||||||
|
let is_admin = token.claims().groups.contains("lldap_admin");
|
||||||
|
Ok(ValidationResults {
|
||||||
|
user: token.claims().user.clone(),
|
||||||
|
is_admin,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn token_validator<Backend>(
|
pub async fn token_validator<Backend>(
|
||||||
req: ServiceRequest,
|
req: ServiceRequest,
|
||||||
credentials: BearerAuth,
|
credentials: BearerAuth,
|
||||||
@ -348,24 +391,9 @@ where
|
|||||||
let state = req
|
let state = req
|
||||||
.app_data::<web::Data<AppState<Backend>>>()
|
.app_data::<web::Data<AppState<Backend>>>()
|
||||||
.expect("Invalid app config");
|
.expect("Invalid app config");
|
||||||
let token: Token<_> = VerifyWithKey::verify_with_key(credentials.token(), &state.jwt_key)
|
let ValidationResults { user, is_admin } = check_if_token_is_valid(state, credentials.token())?;
|
||||||
.map_err(|_| ErrorUnauthorized("Invalid JWT"))?;
|
if is_admin || (!admin_required && req.match_info().get("user_id") == Some(&user)) {
|
||||||
if token.claims().exp.lt(&Utc::now()) {
|
debug!("Got authorized token for user {}", &user);
|
||||||
return Err(ErrorUnauthorized("Expired JWT"));
|
|
||||||
}
|
|
||||||
let jwt_hash = {
|
|
||||||
let mut s = DefaultHasher::new();
|
|
||||||
credentials.token().hash(&mut s);
|
|
||||||
s.finish()
|
|
||||||
};
|
|
||||||
if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) {
|
|
||||||
return Err(ErrorUnauthorized("JWT was logged out"));
|
|
||||||
}
|
|
||||||
let is_admin = token.claims().groups.contains("lldap_admin");
|
|
||||||
if is_admin
|
|
||||||
|| (!admin_required && req.match_info().get("user_id") == Some(&token.claims().user))
|
|
||||||
{
|
|
||||||
debug!("Got authorized token for user {}", &token.claims().user);
|
|
||||||
Ok(req)
|
Ok(req)
|
||||||
} else {
|
} else {
|
||||||
Err(ErrorUnauthorized(
|
Err(ErrorUnauthorized(
|
||||||
|
70
src/infra/graphql/api.rs
Normal file
70
src/infra/graphql/api.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
use crate::{
|
||||||
|
domain::handler::BackendHandler,
|
||||||
|
infra::{
|
||||||
|
auth_service::{check_if_token_is_valid, ValidationResults},
|
||||||
|
tcp_server::AppState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use actix_web::{web, Error, HttpResponse};
|
||||||
|
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
||||||
|
use juniper::{EmptyMutation, EmptySubscription, RootNode};
|
||||||
|
use juniper_actix::{graphiql_handler, graphql_handler, playground_handler};
|
||||||
|
|
||||||
|
use super::query::Query;
|
||||||
|
|
||||||
|
pub struct Context<Handler: BackendHandler> {
|
||||||
|
pub handler: Box<Handler>,
|
||||||
|
pub validation_result: ValidationResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> juniper::Context for Context<Handler> {}
|
||||||
|
|
||||||
|
type Schema<Handler> = RootNode<
|
||||||
|
'static,
|
||||||
|
Query<Handler>,
|
||||||
|
EmptyMutation<Context<Handler>>,
|
||||||
|
EmptySubscription<Context<Handler>>,
|
||||||
|
>;
|
||||||
|
|
||||||
|
fn schema<Handler: BackendHandler + Sync>() -> Schema<Handler> {
|
||||||
|
Schema::new(
|
||||||
|
Query::<Handler>::new(),
|
||||||
|
EmptyMutation::<Context<Handler>>::new(),
|
||||||
|
EmptySubscription::<Context<Handler>>::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn graphiql_route() -> Result<HttpResponse, Error> {
|
||||||
|
graphiql_handler("/api/graphql", None).await
|
||||||
|
}
|
||||||
|
async fn playground_route() -> Result<HttpResponse, Error> {
|
||||||
|
playground_handler("/api/graphql", None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn graphql_route<Handler: BackendHandler + Sync>(
|
||||||
|
req: actix_web::HttpRequest,
|
||||||
|
mut payload: actix_web::web::Payload,
|
||||||
|
data: web::Data<AppState<Handler>>,
|
||||||
|
) -> Result<HttpResponse, Error> {
|
||||||
|
use actix_web::FromRequest;
|
||||||
|
let bearer = BearerAuth::from_request(&req, &mut payload.0).await?;
|
||||||
|
let validation_result = check_if_token_is_valid(&data, bearer.token())?;
|
||||||
|
let context = Context::<Handler> {
|
||||||
|
handler: Box::new(data.backend_handler.clone()),
|
||||||
|
validation_result,
|
||||||
|
};
|
||||||
|
graphql_handler(&schema(), &context, req, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure_endpoint<Backend>(cfg: &mut web::ServiceConfig)
|
||||||
|
where
|
||||||
|
Backend: BackendHandler + Sync + 'static,
|
||||||
|
{
|
||||||
|
cfg.service(
|
||||||
|
web::resource("/graphql")
|
||||||
|
.route(web::post().to(graphql_route::<Backend>))
|
||||||
|
.route(web::get().to(graphql_route::<Backend>)),
|
||||||
|
);
|
||||||
|
cfg.service(web::resource("/graphql/playground").route(web::get().to(playground_route)));
|
||||||
|
cfg.service(web::resource("/graphql/graphiql").route(web::get().to(graphiql_route)));
|
||||||
|
}
|
2
src/infra/graphql/mod.rs
Normal file
2
src/infra/graphql/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod query;
|
340
src/infra/graphql/query.rs
Normal file
340
src/infra/graphql/query.rs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
use crate::domain::handler::BackendHandler;
|
||||||
|
use juniper::{graphql_object, FieldResult, GraphQLInputObject};
|
||||||
|
use lldap_model::{ListUsersRequest, UserDetailsRequest};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use super::api::Context;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||||
|
/// A filter for requests, specifying a boolean expression based on field constraints. Only one of
|
||||||
|
/// the fields can be set at a time.
|
||||||
|
pub struct RequestFilter {
|
||||||
|
any: Option<Vec<RequestFilter>>,
|
||||||
|
all: Option<Vec<RequestFilter>>,
|
||||||
|
not: Option<Box<RequestFilter>>,
|
||||||
|
eq: Option<EqualityConstraint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<lldap_model::RequestFilter> for RequestFilter {
|
||||||
|
type Error = String;
|
||||||
|
fn try_into(self) -> Result<lldap_model::RequestFilter, Self::Error> {
|
||||||
|
let mut field_count = 0;
|
||||||
|
if self.any.is_some() {
|
||||||
|
field_count += 1;
|
||||||
|
}
|
||||||
|
if self.all.is_some() {
|
||||||
|
field_count += 1;
|
||||||
|
}
|
||||||
|
if self.not.is_some() {
|
||||||
|
field_count += 1;
|
||||||
|
}
|
||||||
|
if self.eq.is_some() {
|
||||||
|
field_count += 1;
|
||||||
|
}
|
||||||
|
if field_count == 0 {
|
||||||
|
return Err("No field specified in request filter".to_string());
|
||||||
|
}
|
||||||
|
if field_count > 1 {
|
||||||
|
return Err("Multiple fields specified in request filter".to_string());
|
||||||
|
}
|
||||||
|
if let Some(e) = self.eq {
|
||||||
|
return Ok(lldap_model::RequestFilter::Equality(e.field, e.value));
|
||||||
|
}
|
||||||
|
if let Some(c) = self.any {
|
||||||
|
return Ok(lldap_model::RequestFilter::Or(
|
||||||
|
c.into_iter()
|
||||||
|
.map(TryInto::try_into)
|
||||||
|
.collect::<Result<Vec<_>, String>>()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(c) = self.all {
|
||||||
|
return Ok(lldap_model::RequestFilter::And(
|
||||||
|
c.into_iter()
|
||||||
|
.map(TryInto::try_into)
|
||||||
|
.collect::<Result<Vec<_>, String>>()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if let Some(c) = self.not {
|
||||||
|
return Ok(lldap_model::RequestFilter::Not(Box::new((*c).try_into()?)));
|
||||||
|
}
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, GraphQLInputObject)]
|
||||||
|
pub struct EqualityConstraint {
|
||||||
|
field: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
|
/// The top-level GraphQL query type.
|
||||||
|
pub struct Query<Handler: BackendHandler> {
|
||||||
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> Query<Handler> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context<Handler>)]
|
||||||
|
impl<Handler: BackendHandler + Sync> Query<Handler> {
|
||||||
|
fn api_version() -> &'static str {
|
||||||
|
"1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn user(context: &Context<Handler>, user_id: String) -> FieldResult<User<Handler>> {
|
||||||
|
if !context.validation_result.can_access(&user_id) {
|
||||||
|
return Err("Unauthorized access to user data".into());
|
||||||
|
}
|
||||||
|
Ok(context
|
||||||
|
.handler
|
||||||
|
.get_user_details(UserDetailsRequest { user_id })
|
||||||
|
.await
|
||||||
|
.map(Into::into)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn users(
|
||||||
|
context: &Context<Handler>,
|
||||||
|
#[graphql(name = "where")] filters: Option<RequestFilter>,
|
||||||
|
) -> FieldResult<Vec<User<Handler>>> {
|
||||||
|
if !context.validation_result.is_admin {
|
||||||
|
return Err("Unauthorized access to user list".into());
|
||||||
|
}
|
||||||
|
Ok(context
|
||||||
|
.handler
|
||||||
|
.list_users(ListUsersRequest {
|
||||||
|
filters: match filters {
|
||||||
|
None => None,
|
||||||
|
Some(f) => Some(f.try_into()?),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|v| v.into_iter().map(Into::into).collect())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
/// Represents a single user.
|
||||||
|
pub struct User<Handler: BackendHandler> {
|
||||||
|
user: lldap_model::User,
|
||||||
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> Default for User<Handler> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
user: lldap_model::User::default(),
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context<Handler>)]
|
||||||
|
impl<Handler: BackendHandler + Sync> User<Handler> {
|
||||||
|
fn id(&self) -> &str {
|
||||||
|
&self.user.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn email(&self) -> &str {
|
||||||
|
&self.user.email
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The groups to which this user belongs.
|
||||||
|
async fn groups(&self, context: &Context<Handler>) -> FieldResult<Vec<Group<Handler>>> {
|
||||||
|
Ok(context
|
||||||
|
.handler
|
||||||
|
.get_user_groups(self.user.user_id.clone())
|
||||||
|
.await
|
||||||
|
.map(|set| set.into_iter().map(Into::into).collect())?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> From<lldap_model::User> for User<Handler> {
|
||||||
|
fn from(user: lldap_model::User) -> Self {
|
||||||
|
Self {
|
||||||
|
user,
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||||
|
/// Represents a single group.
|
||||||
|
pub struct Group<Handler: BackendHandler> {
|
||||||
|
group_id: String,
|
||||||
|
_phantom: std::marker::PhantomData<Box<Handler>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object(context = Context<Handler>)]
|
||||||
|
impl<Handler: BackendHandler + Sync> Group<Handler> {
|
||||||
|
fn id(&self) -> String {
|
||||||
|
self.group_id.clone()
|
||||||
|
}
|
||||||
|
/// The groups to which this user belongs.
|
||||||
|
async fn users(&self, context: &Context<Handler>) -> FieldResult<Vec<User<Handler>>> {
|
||||||
|
if !context.validation_result.is_admin {
|
||||||
|
return Err("Unauthorized access to group data".into());
|
||||||
|
}
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Handler: BackendHandler> From<String> for Group<Handler> {
|
||||||
|
fn from(group_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
group_id,
|
||||||
|
_phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{domain::handler::MockTestBackendHandler, infra::auth_service::ValidationResults};
|
||||||
|
use juniper::{
|
||||||
|
execute, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType,
|
||||||
|
RootNode, Variables,
|
||||||
|
};
|
||||||
|
use mockall::predicate::eq;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
fn schema<'q, C, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation<C>, EmptySubscription<C>>
|
||||||
|
where
|
||||||
|
Q: GraphQLType<DefaultScalarValue, Context = C, TypeInfo = ()> + 'q,
|
||||||
|
{
|
||||||
|
RootNode::new(
|
||||||
|
query_root,
|
||||||
|
EmptyMutation::<C>::new(),
|
||||||
|
EmptySubscription::<C>::new(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_user_by_id() {
|
||||||
|
const QUERY: &str = r#"{
|
||||||
|
user(userId: "bob") {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
groups {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
mock.expect_get_user_details()
|
||||||
|
.with(eq(UserDetailsRequest {
|
||||||
|
user_id: "bob".to_string(),
|
||||||
|
}))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(lldap_model::User {
|
||||||
|
user_id: "bob".to_string(),
|
||||||
|
email: "bob@bobbers.on".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let mut groups = HashSet::<String>::new();
|
||||||
|
groups.insert("Bobbersons".to_string());
|
||||||
|
mock.expect_get_user_groups()
|
||||||
|
.with(eq("bob".to_string()))
|
||||||
|
.return_once(|_| Ok(groups));
|
||||||
|
|
||||||
|
let context = Context::<MockTestBackendHandler> {
|
||||||
|
handler: Box::new(mock),
|
||||||
|
validation_result: ValidationResults::admin(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||||
|
assert_eq!(
|
||||||
|
execute(QUERY, None, &schema, &Variables::new(), &context).await,
|
||||||
|
Ok((
|
||||||
|
graphql_value!(
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "bob",
|
||||||
|
"email": "bob@bobbers.on",
|
||||||
|
"groups": [{"id": "Bobbersons"}]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
vec![]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_users() {
|
||||||
|
const QUERY: &str = r#"{
|
||||||
|
users(filters: {
|
||||||
|
any: [
|
||||||
|
{eq: {
|
||||||
|
field: "id"
|
||||||
|
value: "bob"
|
||||||
|
}},
|
||||||
|
{eq: {
|
||||||
|
field: "email"
|
||||||
|
value: "robert@bobbers.on"
|
||||||
|
}}
|
||||||
|
]}) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let mut mock = MockTestBackendHandler::new();
|
||||||
|
use lldap_model::{RequestFilter, User};
|
||||||
|
mock.expect_list_users()
|
||||||
|
.with(eq(ListUsersRequest {
|
||||||
|
filters: Some(RequestFilter::Or(vec![
|
||||||
|
RequestFilter::Equality("id".to_string(), "bob".to_string()),
|
||||||
|
RequestFilter::Equality("email".to_string(), "robert@bobbers.on".to_string()),
|
||||||
|
])),
|
||||||
|
}))
|
||||||
|
.return_once(|_| {
|
||||||
|
Ok(vec![
|
||||||
|
User {
|
||||||
|
user_id: "bob".to_string(),
|
||||||
|
email: "bob@bobbers.on".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
User {
|
||||||
|
user_id: "robert".to_string(),
|
||||||
|
email: "robert@bobbers.on".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
let context = Context::<MockTestBackendHandler> {
|
||||||
|
handler: Box::new(mock),
|
||||||
|
validation_result: ValidationResults::admin(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let schema = schema(Query::<MockTestBackendHandler>::new());
|
||||||
|
assert_eq!(
|
||||||
|
execute(QUERY, None, &schema, &Variables::new(), &context).await,
|
||||||
|
Ok((
|
||||||
|
graphql_value!(
|
||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "bob",
|
||||||
|
"email": "bob@bobbers.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "robert",
|
||||||
|
"email": "robert@bobbers.on"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
vec![]
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ pub mod auth_service;
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod configuration;
|
pub mod configuration;
|
||||||
pub mod db_cleaner;
|
pub mod db_cleaner;
|
||||||
|
pub mod graphql;
|
||||||
pub mod jwt_sql_tables;
|
pub mod jwt_sql_tables;
|
||||||
pub mod ldap_handler;
|
pub mod ldap_handler;
|
||||||
pub mod ldap_server;
|
pub mod ldap_server;
|
||||||
|
@ -62,7 +62,7 @@ where
|
|||||||
|
|
||||||
pub fn api_config<Backend>(cfg: &mut web::ServiceConfig)
|
pub fn api_config<Backend>(cfg: &mut web::ServiceConfig)
|
||||||
where
|
where
|
||||||
Backend: TcpBackendHandler + BackendHandler + 'static,
|
Backend: TcpBackendHandler + BackendHandler + Sync + 'static,
|
||||||
{
|
{
|
||||||
let json_config = web::JsonConfig::default()
|
let json_config = web::JsonConfig::default()
|
||||||
.limit(4096)
|
.limit(4096)
|
||||||
@ -77,6 +77,7 @@ where
|
|||||||
.into()
|
.into()
|
||||||
});
|
});
|
||||||
cfg.app_data(json_config);
|
cfg.app_data(json_config);
|
||||||
|
super::graphql::api::configure_endpoint::<Backend>(cfg);
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::resource("/user/{user_id}")
|
web::resource("/user/{user_id}")
|
||||||
.route(web::get().to(user_details_handler::<Backend>))
|
.route(web::get().to(user_details_handler::<Backend>))
|
||||||
|
@ -47,7 +47,7 @@ fn http_config<Backend>(
|
|||||||
jwt_secret: String,
|
jwt_secret: String,
|
||||||
jwt_blacklist: HashSet<u64>,
|
jwt_blacklist: HashSet<u64>,
|
||||||
) where
|
) where
|
||||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + 'static,
|
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static,
|
||||||
{
|
{
|
||||||
cfg.app_data(web::Data::new(AppState::<Backend> {
|
cfg.app_data(web::Data::new(AppState::<Backend> {
|
||||||
backend_handler,
|
backend_handler,
|
||||||
@ -84,7 +84,7 @@ pub async fn build_tcp_server<Backend>(
|
|||||||
server_builder: ServerBuilder,
|
server_builder: ServerBuilder,
|
||||||
) -> Result<ServerBuilder>
|
) -> Result<ServerBuilder>
|
||||||
where
|
where
|
||||||
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + 'static,
|
Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static,
|
||||||
{
|
{
|
||||||
let jwt_secret = config.jwt_secret.clone();
|
let jwt_secret = config.jwt_secret.clone();
|
||||||
let jwt_blacklist = backend_handler.get_jwt_blacklist().await?;
|
let jwt_blacklist = backend_handler.get_jwt_blacklist().await?;
|
||||||
|
Loading…
Reference in New Issue
Block a user