kanidm/kanidmd/src/lib/core/mod.rs

1568 lines
53 KiB
Rust
Raw Normal View History

// use actix_files as fs;
use actix::prelude::*;
use actix_session::{CookieSession, Session};
use actix_web::web::{self, Data, HttpResponse, Json, Path};
use actix_web::{cookie, error, middleware, App, HttpServer};
use std::sync::Arc;
use time::Duration;
use crate::config::Configuration;
// SearchResult
use crate::actors::v1_read::QueryServerReadV1;
use crate::actors::v1_read::{
AuthMessage, IdmAccountUnixAuthMessage, InternalRadiusReadMessage,
InternalRadiusTokenReadMessage, InternalSearchMessage, InternalSshKeyReadMessage,
InternalSshKeyTagReadMessage, InternalUnixGroupTokenReadMessage,
InternalUnixUserTokenReadMessage, SearchMessage, WhoamiMessage,
};
use crate::actors::v1_write::QueryServerWriteV1;
use crate::actors::v1_write::{
AppendAttributeMessage, CreateMessage, DeleteMessage, IdmAccountSetPasswordMessage,
IdmAccountUnixExtendMessage, IdmAccountUnixSetCredMessage, IdmGroupUnixExtendMessage,
InternalCredentialSetMessage, InternalDeleteMessage, InternalRegenerateRadiusMessage,
InternalSshKeyCreateMessage, ModifyMessage, PurgeAttributeMessage, RemoveAttributeValueMessage,
SetAttributeMessage,
};
use crate::async_log;
use crate::audit::AuditScope;
use crate::be::{Backend, BackendTransaction};
use crate::crypto::setup_tls;
use crate::filter::{Filter, FilterInvalid};
use crate::idm::server::IdmServer;
use crate::interval::IntervalActor;
use crate::schema::Schema;
use crate::schema::SchemaTransaction;
use crate::server::QueryServer;
use crate::utils::SID;
use crate::value::PartialValue;
use kanidm_proto::v1::Entry as ProtoEntry;
use kanidm_proto::v1::OperationError;
use kanidm_proto::v1::{
AccountUnixExtend, AuthRequest, AuthState, CreateRequest, DeleteRequest, GroupUnixExtend,
ModifyRequest, SearchRequest, SetAuthCredential, SingleStringRequest, UserAuthToken,
};
use uuid::Uuid;
struct AppState {
qe_r: Addr<QueryServerReadV1>,
qe_w: Addr<QueryServerWriteV1>,
}
fn get_current_user(session: &Session) -> Option<UserAuthToken> {
match session.get::<UserAuthToken>("uat") {
Ok(maybe_uat) => maybe_uat,
Err(_) => None,
}
}
fn operation_error_to_response(e: OperationError) -> HttpResponse {
match e {
OperationError::NotAuthenticated => HttpResponse::Unauthorized().json(e),
OperationError::AccessDenied | OperationError::SystemProtectedObject => {
HttpResponse::Forbidden().json(e)
}
OperationError::EmptyRequest
| OperationError::NoMatchingEntries
| OperationError::SchemaViolation(_) => HttpResponse::BadRequest().json(e),
_ => HttpResponse::InternalServerError().json(e),
}
}
macro_rules! json_event_post {
($req:expr, $session:expr, $message_type:ty, $dest:expr) => {{
// Get auth if any?
let uat = get_current_user(&$session);
// Send to the db for handling
// combine request + uat -> message.
let m_obj = <$message_type>::new(uat, $req);
match $dest.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}};
}
macro_rules! json_event_get {
($session:expr, $state:expr, $message_type:ty) => {{
// Get current auth data - remember, the QS checks if the
// none/some is okay, because it's too hard to make it work here
// with all the async parts.
let uat = get_current_user(&$session);
// New event, feed current auth data from the token to it.
let obj = <$message_type>::new(uat);
match $state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}};
}
// Handle the various end points we need to expose
async fn create(
(req, session, state): (Json<CreateRequest>, Session, Data<AppState>),
) -> HttpResponse {
json_event_post!(req.into_inner(), session, CreateMessage, state.qe_w)
}
async fn modify(
(req, session, state): (Json<ModifyRequest>, Session, Data<AppState>),
) -> HttpResponse {
json_event_post!(req.into_inner(), session, ModifyMessage, state.qe_w)
}
async fn delete(
(req, session, state): (Json<DeleteRequest>, Session, Data<AppState>),
) -> HttpResponse {
json_event_post!(req.into_inner(), session, DeleteMessage, state.qe_w)
}
async fn search(
(req, session, state): (Json<SearchRequest>, Session, Data<AppState>),
) -> HttpResponse {
json_event_post!(req.into_inner(), session, SearchMessage, state.qe_r)
}
async fn whoami((session, state): (Session, Data<AppState>)) -> HttpResponse {
json_event_get!(session, state, WhoamiMessage)
}
// =============== REST generics ========================
async fn json_rest_event_get(
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
attrs: Option<Vec<String>>,
) -> HttpResponse {
let uat = get_current_user(&session);
let obj = InternalSearchMessage { uat, filter, attrs };
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_get_id(
path: Path<String>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
attrs: Option<Vec<String>>,
) -> HttpResponse {
let uat = get_current_user(&session);
let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str())));
let obj = InternalSearchMessage { uat, filter, attrs };
match state.qe_r.send(obj).await {
Ok(Ok(mut r)) => HttpResponse::Ok().json(r.pop()),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_delete_id(
path: Path<String>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
) -> HttpResponse {
let uat = get_current_user(&session);
let filter = Filter::join_parts_and(filter, filter_all!(f_id(path.as_str())));
let obj = InternalDeleteMessage { uat, filter };
match state.qe_w.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_get_id_attr(
path: Path<(String, String)>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
) -> HttpResponse {
let (id, attr) = path.into_inner();
let uat = get_current_user(&session);
let filter = Filter::join_parts_and(filter, filter_all!(f_id(id.as_str())));
let obj = InternalSearchMessage {
uat,
filter,
attrs: Some(vec![attr.clone()]),
};
match state.qe_r.send(obj).await {
Ok(Ok(mut event_result)) => {
// TODO: Check this only has len 1, even though that satte should be impossible.
// Only get one result
let r = event_result.pop().and_then(|mut e| {
// Only get the attribute as requested.
e.attrs.remove(&attr)
});
debug!("final json result {:?}", r);
// Only send back the first result, or None
HttpResponse::Ok().json(r)
}
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_post(
mut obj: ProtoEntry,
session: Session,
state: Data<AppState>,
classes: Vec<String>,
) -> HttpResponse {
// Read the json from the wire.
let uat = get_current_user(&session);
obj.attrs.insert("class".to_string(), classes);
let m_obj = CreateMessage::new_entry(uat, obj);
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_post_id_attr(
path: Path<(String, String)>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
values: Vec<String>,
) -> HttpResponse {
let uat = get_current_user(&session);
let (id, attr) = path.into_inner();
let m_obj = AppendAttributeMessage {
uat,
uuid_or_name: id,
attr,
values,
filter,
};
// Add a msg here
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_put_id_attr(
path: Path<(String, String)>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
values: Vec<String>,
) -> HttpResponse {
let uat = get_current_user(&session);
let (id, attr) = path.into_inner();
let m_obj = SetAttributeMessage {
uat,
uuid_or_name: id,
attr,
values,
filter,
};
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_delete_id_attr(
path: Path<(String, String)>,
session: Session,
state: Data<AppState>,
filter: Filter<FilterInvalid>,
) -> HttpResponse {
let uat = get_current_user(&session);
let (id, attr) = path.into_inner();
// TODO: Attempt to get an option Vec<String> here?
let obj = PurgeAttributeMessage {
uat,
uuid_or_name: id,
attr,
filter,
};
match state.qe_w.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn json_rest_event_credential_put(
id: String,
cred_id: Option<String>,
session: Session,
state: Data<AppState>,
obj: SetAuthCredential,
) -> HttpResponse {
let uat = get_current_user(&session);
let m_obj = InternalCredentialSetMessage::new(uat, id, cred_id, obj);
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
// Okay, so a put normally needs
// * filter of what we are working on (id + class)
// * a BTreeMap<String, Vec<String>> that we turn into a modlist.
//
// OR
// * filter of what we are working on (id + class)
// * a Vec<String> that we are changing
// * the attr name (as a param to this in path)
//
// json_rest_event_put_id(path, req, state
async fn schema_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
// NOTE: This is filter_all, because from_internal_message will still do the alterations
// needed to make it safe. This is needed because there may be aci's that block access
// to the recycle/ts types in the filter, and we need the aci to only eval on this
// part of the filter!
let filter = filter_all!(f_or!([
f_eq("class", PartialValue::new_class("attributetype")),
f_eq("class", PartialValue::new_class("classtype"))
]));
json_rest_event_get(session, state, filter, None).await
}
async fn schema_attributetype_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("attributetype")));
json_rest_event_get(session, state, filter, None).await
}
async fn schema_attributetype_get_id(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
// These can't use get_id because they attribute name and class name aren't ... well name.
let uat = get_current_user(&session);
let filter = filter_all!(f_and!([
f_eq("class", PartialValue::new_class("attributetype")),
f_eq("attributename", PartialValue::new_iutf8s(path.as_str()))
]));
let obj = InternalSearchMessage {
uat,
filter,
attrs: None,
};
match state.qe_r.send(obj).await {
Ok(Ok(mut r)) => HttpResponse::Ok().json(r.pop()),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn schema_classtype_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("classtype")));
json_rest_event_get(session, state, filter, None).await
}
async fn schema_classtype_get_id(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
// These can't use get_id because they attribute name and class name aren't ... well name.
let uat = get_current_user(&session);
let filter = filter_all!(f_and!([
f_eq("class", PartialValue::new_class("classtype")),
f_eq("classname", PartialValue::new_iutf8s(path.as_str()))
]));
let obj = InternalSearchMessage {
uat,
filter,
attrs: None,
};
match state.qe_r.send(obj).await {
Ok(Ok(mut r)) => HttpResponse::Ok().json(r.pop()),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get(session, state, filter, None).await
}
async fn account_post(
(obj, session, state): (Json<ProtoEntry>, Session, Data<AppState>),
) -> HttpResponse {
let classes = vec!["account".to_string(), "object".to_string()];
json_rest_event_post(obj.into_inner(), session, state, classes).await
}
async fn account_id_get(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get_id(path, session, state, filter, None).await
}
async fn account_id_get_attr(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_get_id_attr(path, session, state, filter).await
}
async fn account_id_post_attr(
(values, path, session, state): (
Json<Vec<String>>,
Path<(String, String)>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_post_id_attr(path, session, state, filter, values.into_inner()).await
}
async fn account_id_delete_attr(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_delete_id_attr(path, session, state, filter).await
}
async fn account_id_put_attr(
(values, path, session, state): (
Json<Vec<String>>,
Path<(String, String)>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_put_id_attr(path, session, state, filter, values.into_inner()).await
}
async fn account_id_delete(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_delete_id(path, session, state, filter).await
}
async fn account_put_id_credential_primary(
(obj, path, session, state): (
Json<SetAuthCredential>,
Path<String>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let id = path.into_inner();
json_rest_event_credential_put(id, None, session, state, obj.into_inner()).await
}
// Return a vec of str
async fn account_get_id_ssh_pubkeys(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalSshKeyReadMessage {
uat,
uuid_or_name: id,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_post_id_ssh_pubkey(
(obj, path, session, state): (
Json<(String, String)>,
Path<String>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let (tag, key) = obj.into_inner();
let m_obj = InternalSshKeyCreateMessage {
uat,
uuid_or_name: id,
tag,
key,
filter: filter_all!(f_eq("class", PartialValue::new_class("account"))),
};
// Add a msg here
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_get_id_ssh_pubkey_tag(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let (id, tag) = path.into_inner();
let obj = InternalSshKeyTagReadMessage {
uat,
uuid_or_name: id,
tag,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_delete_id_ssh_pubkey_tag(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let (id, tag) = path.into_inner();
let obj = RemoveAttributeValueMessage {
uat,
uuid_or_name: id,
attr: "ssh_publickey".to_string(),
value: tag,
filter: filter_all!(f_eq("class", PartialValue::new_class("account"))),
};
match state.qe_w.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
// Get and return a single str
async fn account_get_id_radius(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalRadiusReadMessage {
uat,
uuid_or_name: id,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_post_id_radius_regenerate(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
// Need to to send the regen msg
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalRegenerateRadiusMessage::new(uat, id);
match state.qe_w.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_delete_id_radius(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
// We reconstruct path here to keep json_rest_event_delete_id_attr generic.
let p = Path::from((path.into_inner(), "radius_secret".to_string()));
let filter = filter_all!(f_eq("class", PartialValue::new_class("account")));
json_rest_event_delete_id_attr(p, session, state, filter).await
}
async fn account_get_id_radius_token(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalRadiusTokenReadMessage {
uat,
uuid_or_name: id,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_post_id_unix(
(obj, path, session, state): (
Json<AccountUnixExtend>,
Path<String>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let m_obj = IdmAccountUnixExtendMessage::new(uat, id, obj.into_inner());
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_get_id_unix_token(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalUnixUserTokenReadMessage {
uat,
uuid_or_name: id,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_post_id_unix_auth(
(obj, path, session, state): (
Json<SingleStringRequest>,
Path<String>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let m_obj = IdmAccountUnixAuthMessage {
uat: uat,
uuid_or_name: id,
cred: obj.into_inner().value,
};
match state.qe_r.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_put_id_unix_credential(
(obj, path, session, state): (
Json<SingleStringRequest>,
Path<String>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let m_obj = IdmAccountUnixSetCredMessage {
uat,
uuid_or_name: id,
cred: obj.into_inner().value,
};
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn account_delete_id_unix_credential(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = PurgeAttributeMessage {
uat,
uuid_or_name: id,
attr: "unix_password".to_string(),
filter: filter_all!(f_eq("class", PartialValue::new_class("posixaccount"))),
};
match state.qe_w.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn group_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_get(session, state, filter, None).await
}
async fn group_post(
(obj, session, state): (Json<ProtoEntry>, Session, Data<AppState>),
) -> HttpResponse {
let classes = vec!["group".to_string(), "object".to_string()];
json_rest_event_post(obj.into_inner(), session, state, classes).await
}
async fn group_id_get(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_get_id(path, session, state, filter, None).await
}
async fn group_id_get_attr(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_get_id_attr(path, session, state, filter).await
}
async fn group_id_post_attr(
(values, path, session, state): (
Json<Vec<String>>,
Path<(String, String)>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_post_id_attr(path, session, state, filter, values.into_inner()).await
}
async fn group_id_delete_attr(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_delete_id_attr(path, session, state, filter).await
}
async fn group_id_put_attr(
(values, path, session, state): (
Json<Vec<String>>,
Path<(String, String)>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_put_id_attr(path, session, state, filter, values.into_inner()).await
}
async fn group_id_delete(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("group")));
json_rest_event_delete_id(path, session, state, filter).await
}
async fn group_post_id_unix(
(obj, path, session, state): (Json<GroupUnixExtend>, Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let m_obj = IdmGroupUnixExtendMessage::new(uat, id, obj.into_inner());
match state.qe_w.send(m_obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn group_get_id_unix_token(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let uat = get_current_user(&session);
let id = path.into_inner();
let obj = InternalUnixGroupTokenReadMessage {
uat,
uuid_or_name: id,
};
match state.qe_r.send(obj).await {
Ok(Ok(r)) => HttpResponse::Ok().json(r),
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn domain_get((session, state): (Session, Data<AppState>)) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_get(session, state, filter, None).await
}
async fn domain_id_get(
(path, session, state): (Path<String>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_get_id(path, session, state, filter, None).await
}
async fn domain_id_get_attr(
(path, session, state): (Path<(String, String)>, Session, Data<AppState>),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_get_id_attr(path, session, state, filter).await
}
async fn domain_id_put_attr(
(values, path, session, state): (
Json<Vec<String>>,
Path<(String, String)>,
Session,
Data<AppState>,
),
) -> HttpResponse {
let filter = filter_all!(f_eq("class", PartialValue::new_class("domain_info")));
json_rest_event_put_id_attr(path, session, state, filter, values.into_inner()).await
}
async fn do_nothing(_session: Session) -> String {
"did nothing".to_string()
}
// We probably need an extract auth or similar to handle the different
// types (cookie, bearer), and to generic this over get/post.
async fn auth((obj, session, state): (Json<AuthRequest>, Session, Data<AppState>)) -> HttpResponse {
// First, deal with some state management.
// Do anything here first that's needed like getting the session details
// out of the req cookie.
// From the actix source errors here
// seems to be related to the serde_json deserialise of the cookie
// content, and because we control it's get/set it SHOULD be fine
// provided we use secure cookies. But we can't always trust that ...
let maybe_sessionid = match session.get::<Uuid>("auth-session-id") {
Ok(c) => c,
Err(_e) => return HttpResponse::InternalServerError().json(()),
};
let auth_msg = AuthMessage::new(obj.into_inner(), maybe_sessionid);
// We probably need to know if we allocate the cookie, that this is a
// new session, and in that case, anything *except* authrequest init is
// invalid.
match state
// This may change in the future ...
.qe_r
.send(auth_msg)
.await
{
Ok(Ok(ar)) => {
match &ar.state {
AuthState::Success(uat) => {
// Remove the auth-session-id
session.remove("auth-session-id");
// Set the uat into the cookie
match session.set("uat", uat) {
Ok(_) => HttpResponse::Ok().json(ar),
Err(_) => HttpResponse::InternalServerError().json(()),
}
}
AuthState::Denied(_) => {
// Remove the auth-session-id
session.remove("auth-session-id");
HttpResponse::Unauthorized().json(ar)
}
AuthState::Continue(_) => {
// Ensure the auth-session-id is set
match session.set("auth-session-id", ar.sessionid) {
Ok(_) => HttpResponse::Ok().json(ar),
Err(_) => HttpResponse::InternalServerError().json(()),
}
}
}
}
Ok(Err(e)) => operation_error_to_response(e),
Err(_) => HttpResponse::InternalServerError().json("mailbox failure"),
}
}
async fn idm_account_set_password(
(obj, session, state): (Json<SingleStringRequest>, Session, Data<AppState>),
) -> HttpResponse {
json_event_post!(
obj.into_inner(),
session,
IdmAccountSetPasswordMessage,
state.qe_w
)
}
// === internal setup helpers
fn setup_backend(config: &Configuration) -> Result<Backend, OperationError> {
let mut audit_be = AuditScope::new("backend_setup");
let pool_size: u32 = config.threads as u32;
let be = Backend::new(&mut audit_be, config.db_path.as_str(), pool_size);
// debug!
debug!("{}", audit_be);
be
}
// TODO #54: We could move most of the be/schema/qs setup and startup
// outside of this call, then pass in "what we need" in a cloneable
// form, this way we could have seperate Idm vs Qs threads, and dedicated
// threads for write vs read
fn setup_qs_idms(
audit: &mut AuditScope,
be: Backend,
sid: SID,
) -> Result<(QueryServer, IdmServer), OperationError> {
// Create "just enough" schema for us to be able to load from
// disk ... Schema loading is one time where we validate the
// entries as we read them, so we need this here.
let schema = match Schema::new(audit) {
Ok(s) => s,
Err(e) => {
error!("Failed to setup in memory schema: {:?}", e);
return Err(e);
}
};
// Create a query_server implementation
let query_server = QueryServer::new(be, schema);
// TODO #62: Should the IDM parts be broken out to the IdmServer?
// What's important about this initial setup here is that it also triggers
// the schema and acp reload, so they are now configured correctly!
// Initialise the schema core.
//
// Now search for the schema itself, and validate that the system
// in memory matches the BE on disk, and that it's syntactically correct.
// Write it out if changes are needed.
query_server.initialise_helper(audit)?;
// We generate a SINGLE idms only!
let idms = IdmServer::new(query_server.clone(), sid);
Ok((query_server, idms))
}
pub fn backup_server_core(config: Configuration, dst_path: &str) {
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let mut audit = AuditScope::new("backend_backup");
let be_ro_txn = be.read();
let r = be_ro_txn.backup(&mut audit, dst_path);
debug!("{}", audit);
match r {
Ok(_) => info!("Backup success!"),
Err(e) => {
error!("Backup failed: {:?}", e);
std::process::exit(1);
}
};
// Let the txn abort, even on success.
}
pub fn restore_server_core(config: Configuration, dst_path: &str) {
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let mut audit = AuditScope::new("backend_restore");
// First, we provide the in-memory schema so that core attrs are indexed correctly.
let schema = match Schema::new(&mut audit) {
Ok(s) => s,
Err(e) => {
error!("Failed to setup in memory schema: {:?}", e);
std::process::exit(1);
}
};
// Limit the scope of the schema txn.
let idxmeta = { schema.write().get_idxmeta_set() };
let mut be_wr_txn = be.write(idxmeta);
let r = be_wr_txn
.restore(&mut audit, dst_path)
.and_then(|_| be_wr_txn.commit(&mut audit));
if r.is_err() {
debug!("{}", audit);
error!("Failed to restore database: {:?}", r);
std::process::exit(1);
}
info!("Restore Success!");
info!("Attempting to init query server ...");
let server_id = be.get_db_sid();
let (qs, _idms) = match setup_qs_idms(&mut audit, be, server_id) {
Ok(t) => t,
Err(e) => {
debug!("{}", audit);
error!("Unable to setup query server or idm server -> {:?}", e);
return;
}
};
info!("Success!");
info!("Start reindex phase ...");
let qs_write = qs.write();
let r = qs_write
.reindex(&mut audit)
.and_then(|_| qs_write.commit(&mut audit));
match r {
Ok(_) => info!("Reindex Success!"),
Err(e) => {
error!("Restore failed: {:?}", e);
std::process::exit(1);
}
};
}
pub fn reindex_server_core(config: Configuration) {
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let mut audit = AuditScope::new("server_reindex");
// First, we provide the in-memory schema so that core attrs are indexed correctly.
let schema = match Schema::new(&mut audit) {
Ok(s) => s,
Err(e) => {
error!("Failed to setup in memory schema: {:?}", e);
std::process::exit(1);
}
};
info!("Start Index Phase 1 ...");
// Limit the scope of the schema txn.
let idxmeta = { schema.write().get_idxmeta_set() };
// Reindex only the core schema attributes to bootstrap the process.
let be_wr_txn = be.write(idxmeta);
let r = be_wr_txn
.reindex(&mut audit)
.and_then(|_| be_wr_txn.commit(&mut audit));
// Now that's done, setup a minimal qs and reindex from that.
if r.is_err() {
debug!("{}", audit);
error!("Failed to reindex database: {:?}", r);
std::process::exit(1);
}
info!("Index Phase 1 Success!");
info!("Attempting to init query server ...");
let server_id = be.get_db_sid();
let (qs, _idms) = match setup_qs_idms(&mut audit, be, server_id) {
Ok(t) => t,
Err(e) => {
debug!("{}", audit);
error!("Unable to setup query server or idm server -> {:?}", e);
return;
}
};
info!("Init Query Server Success!");
info!("Start Index Phase 2 ...");
let qs_write = qs.write();
let r = qs_write
.reindex(&mut audit)
.and_then(|_| qs_write.commit(&mut audit));
match r {
Ok(_) => info!("Index Phase 2 Success!"),
Err(e) => {
error!("Reindex failed: {:?}", e);
std::process::exit(1);
}
};
}
pub fn domain_rename_core(config: Configuration, new_domain_name: String) {
let mut audit = AuditScope::new("domain_rename");
// Start the backend.
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let server_id = be.get_db_sid();
// setup the qs - *with* init of the migrations and schema.
let (qs, _idms) = match setup_qs_idms(&mut audit, be, server_id) {
Ok(t) => t,
Err(e) => {
debug!("{}", audit);
error!("Unable to setup query server or idm server -> {:?}", e);
return;
}
};
let mut qs_write = qs.write();
let r = qs_write
.domain_rename(&mut audit, new_domain_name.as_str())
.and_then(|_| qs_write.commit(&mut audit));
match r {
Ok(_) => info!("Domain Rename Success!"),
Err(e) => {
error!("Domain Rename Failed - Rollback has occured: {:?}", e);
std::process::exit(1);
}
};
}
pub fn reset_sid_core(config: Configuration) {
let mut audit = AuditScope::new("reset_sid_core");
// Setup the be
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let nsid = be.reset_db_sid(&mut audit);
debug!("{}", audit);
info!("New Server ID: {:?}", nsid);
}
pub fn verify_server_core(config: Configuration) {
let mut audit = AuditScope::new("server_verify");
// Setup the be
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
// setup the qs - without initialise!
let schema_mem = match Schema::new(&mut audit) {
Ok(sc) => sc,
Err(e) => {
error!("Failed to setup in memory schema: {:?}", e);
return;
}
};
let server = QueryServer::new(be, schema_mem);
// Run verifications.
let r = server.verify(&mut audit);
debug!("{}", audit);
if r.is_empty() {
info!("Verification passed!");
std::process::exit(0);
} else {
for er in r {
error!("{:?}", er);
}
std::process::exit(1);
}
// Now add IDM server verifications?
}
pub fn recover_account_core(config: Configuration, name: String, password: String) {
let mut audit = AuditScope::new("recover_account");
// Start the backend.
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE: {:?}", e);
return;
}
};
let server_id = be.get_db_sid();
// setup the qs - *with* init of the migrations and schema.
let (_qs, idms) = match setup_qs_idms(&mut audit, be, server_id) {
Ok(t) => t,
Err(e) => {
debug!("{}", audit);
error!("Unable to setup query server or idm server -> {:?}", e);
return;
}
};
// Run the password change.
let mut idms_prox_write = idms.proxy_write();
match idms_prox_write.recover_account(&mut audit, name, password) {
Ok(_) => {
idms_prox_write
.commit(&mut audit)
.expect("A critical error during commit occured.");
debug!("{}", audit);
info!("Password reset!");
}
Err(e) => {
error!("Error during password reset -> {:?}", e);
debug!("{}", audit);
// abort the txn
std::mem::drop(idms_prox_write);
std::process::exit(1);
}
};
}
pub fn create_server_core(config: Configuration) {
// Until this point, we probably want to write to the log macro fns.
if config.integration_test_config.is_some() {
warn!("RUNNING IN INTEGRATION TEST MODE.");
warn!("IF YOU SEE THIS IN PRODUCTION YOU MUST CONTACT SUPPORT IMMEDIATELY.");
}
info!("Starting kanidm with configuration: {}", config);
// The log server is started on it's own thread, and is contacted
// asynchronously.
let log_addr = async_log::start();
// Setup TLS (if any)
let opt_tls_params = match setup_tls(&config) {
Ok(opt_tls_params) => opt_tls_params,
Err(e) => {
error!("Failed to configure TLS parameters -> {:?}", e);
return;
}
};
// Similar, create a stats thread which aggregates statistics from the
// server as they come in.
// Setup the be for the qs.
let be = match setup_backend(&config) {
Ok(be) => be,
Err(e) => {
error!("Failed to setup BE -> {:?}", e);
return;
}
};
let server_id = be.get_db_sid();
info!("Server ID -> {:?}", server_id);
let mut audit = AuditScope::new("setup_qs_idms");
// Start the IDM server.
let (qs, idms) = match setup_qs_idms(&mut audit, be, server_id) {
Ok(t) => t,
Err(e) => {
debug!("{}", audit);
error!("Unable to setup query server or idm server -> {:?}", e);
return;
}
};
// Any pre-start tasks here.
match &config.integration_test_config {
Some(itc) => {
let mut idms_prox_write = idms.proxy_write();
match idms_prox_write.recover_account(
&mut audit,
"admin".to_string(),
itc.admin_password.clone(),
) {
Ok(_) => {}
Err(e) => {
debug!("{}", audit);
error!(
"Unable to configure INTERGATION TEST admin account -> {:?}",
e
);
return;
}
};
match idms_prox_write.commit(&mut audit) {
Ok(_) => {}
Err(e) => {
debug!("{}", audit);
error!("Unable to commit INTERGATION TEST setup -> {:?}", e);
return;
}
}
}
None => {}
}
log_addr.do_send(audit);
// Arc the idms.
let idms_arc = Arc::new(idms);
// Pass it to the actor for threading.
// Start the read query server with the given be path: future config
let server_read_addr = QueryServerReadV1::start(
log_addr.clone(),
qs.clone(),
idms_arc.clone(),
config.threads,
);
// Start the write thread
let server_write_addr = QueryServerWriteV1::start(log_addr, qs, idms_arc);
// Setup timed events associated to the write thread
let _int_addr = IntervalActor::new(server_write_addr.clone()).start();
// Copy the max size
let secure_cookies = config.secure_cookies;
// domain will come from the qs now!
let cookie_key: [u8; 32] = config.cookie_key;
// start the web server
let server = HttpServer::new(move || {
App::new()
.data(AppState {
qe_r: server_read_addr.clone(),
qe_w: server_write_addr.clone(),
})
.wrap(middleware::Logger::default())
.wrap(
// Signed prevents tampering. this 32 byte key MUST
// be generated (probably a cli option, and it's up to the
// server process to coordinate these on hosts). IE an RODC
// could have a different key than our write servers to prevent
// disclosure of a writeable token in case of compromise. It does
// mean that you can't load balance between the rodc and the write
// though, but that's tottaly reasonable.
CookieSession::signed(&cookie_key)
// .path(prefix.as_str())
// .domain(domain.as_str())
.same_site(cookie::SameSite::Strict)
.name("kanidm-session")
// if true, only allow to https
.secure(secure_cookies)
// TODO #63: make this configurable!
.max_age_time(Duration::hours(1)),
)
// .service(fs::Files::new("/static", "./static"))
// Even though this says "CreateRequest", it's actually configuring *ALL* json requests.
// .app_data(web::Json::<CreateRequest>::configure(|cfg| { cfg
.app_data(
web::JsonConfig::default()
.limit(4096)
.error_handler(|err, _req| {
let s = format!("{}", err);
error::InternalError::from_response(err, HttpResponse::BadRequest().json(s))
.into()
}),
)
.service(
web::scope("/v1/raw")
.route("/create", web::post().to(create))
.route("/modify", web::post().to(modify))
.route("/delete", web::post().to(delete))
.route("/search", web::post().to(search)),
)
.service(web::scope("/v1/auth").route("", web::post().to(auth)))
.service(
web::scope("/v1/schema")
.route("", web::get().to(schema_get))
.route("/attributetype", web::get().to(schema_attributetype_get))
.route("/attributetype", web::post().to(do_nothing))
.route(
"/attributetype/{id}",
web::get().to(schema_attributetype_get_id),
)
.route("/attributetype/{id}", web::put().to(do_nothing))
.route("/attributetype/{id}", web::patch().to(do_nothing))
.route("/classtype", web::get().to(schema_classtype_get))
.route("/classtype", web::post().to(do_nothing))
.route("/classtype/{id}", web::get().to(schema_classtype_get_id))
.route("/classtype/{id}", web::put().to(do_nothing))
.route("/classtype/{id}", web::patch().to(do_nothing)),
)
.service(
web::scope("/v1/self")
.route("", web::get().to(whoami))
.route("/_attr/{attr}", web::get().to(do_nothing))
.route("/_credential", web::get().to(do_nothing))
.route(
"/_credential/primary/set_password",
web::post().to(idm_account_set_password),
)
.route("/_credential/{cid}/_lock", web::get().to(do_nothing))
.route("/_radius", web::get().to(do_nothing))
.route("/_radius", web::delete().to(do_nothing))
.route("/_radius", web::post().to(do_nothing))
.route("/_radius/_config", web::post().to(do_nothing))
.route("/_radius/_config/{secret_otp}", web::get().to(do_nothing))
.route(
"/_radius/_config/{secret_otp}/apple",
web::get().to(do_nothing),
),
)
.service(
web::scope("/v1/account")
.route("", web::get().to(account_get))
.route("", web::post().to(account_post))
.route("/{id}", web::get().to(account_id_get))
.route("/{id}", web::delete().to(account_id_delete))
.route("/{id}/_attr/{attr}", web::get().to(account_id_get_attr))
.route("/{id}/_attr/{attr}", web::post().to(account_id_post_attr))
.route("/{id}/_attr/{attr}", web::put().to(account_id_put_attr))
.route(
"/{id}/_attr/{attr}",
web::delete().to(account_id_delete_attr),
)
.route("/{id}/_lock", web::get().to(do_nothing))
.route("/{id}/_credential", web::get().to(do_nothing))
.route(
"/{id}/_credential/primary",
web::put().to(account_put_id_credential_primary),
)
.route("/{id}/_credential/{cid}/_lock", web::get().to(do_nothing))
.route(
"/{id}/_ssh_pubkeys",
web::get().to(account_get_id_ssh_pubkeys),
)
.route(
"/{id}/_ssh_pubkeys",
web::post().to(account_post_id_ssh_pubkey),
)
.route(
"/{id}/_ssh_pubkeys/{tag}",
web::get().to(account_get_id_ssh_pubkey_tag),
)
.route(
"/{id}/_ssh_pubkeys/{tag}",
web::delete().to(account_delete_id_ssh_pubkey_tag),
)
.route("/{id}/_radius", web::get().to(account_get_id_radius))
.route(
"/{id}/_radius",
web::post().to(account_post_id_radius_regenerate),
)
.route("/{id}/_radius", web::delete().to(account_delete_id_radius))
.route(
"/{id}/_radius/_token",
web::get().to(account_get_id_radius_token),
)
.route("/{id}/_unix", web::post().to(account_post_id_unix))
.route(
"/{id}/_unix/_token",
web::get().to(account_get_id_unix_token),
)
.route(
"/{id}/_unix/_auth",
web::post().to(account_post_id_unix_auth),
)
.route(
"/{id}/_unix/_credential",
web::put().to(account_put_id_unix_credential),
)
.route(
"/{id}/_unix/_credential",
web::delete().to(account_delete_id_unix_credential),
),
)
.service(
web::scope("/v1/group")
.route("", web::get().to(group_get))
.route("", web::post().to(group_post))
.route("/{id}", web::get().to(group_id_get))
.route("/{id}", web::delete().to(group_id_delete))
.route("/{id}/_attr/{attr}", web::get().to(group_id_get_attr))
.route("/{id}/_attr/{attr}", web::post().to(group_id_post_attr))
.route("/{id}/_attr/{attr}", web::put().to(group_id_put_attr))
.route("/{id}/_attr/{attr}", web::delete().to(group_id_delete_attr))
.route("/{id}/_unix", web::post().to(group_post_id_unix))
.route("/{id}/_unix/_token", web::get().to(group_get_id_unix_token)),
)
.service(
web::scope("/v1/domain")
.route("", web::get().to(domain_get))
.route("/{id}", web::get().to(domain_id_get))
.route("/{id}/_attr/{attr}", web::get().to(domain_id_get_attr))
.route("/{id}/_attr/{attr}", web::put().to(domain_id_put_attr)),
)
.service(
web::scope("/v1/recycle_bin")
.route("", web::get().to(do_nothing))
.route("/{id}", web::get().to(do_nothing))
.route("/{id}/_restore", web::get().to(do_nothing)),
)
.service(
web::scope("/v1/access_profile")
.route("", web::get().to(do_nothing))
.route("/{id}", web::get().to(do_nothing))
.route("/{id}/_attr/{attr}", web::get().to(do_nothing)),
)
});
let server = match opt_tls_params {
Some(tls_params) => server.bind_openssl(config.address, tls_params),
None => {
warn!("Starting WITHOUT TLS parameters. This may cause authentication to fail!");
server.bind(config.address)
}
};
server.expect("Failed to initialise server!").run();
}