kanidmd_core/https/views/admin/
persons.rs

1use crate::https::errors::WebError;
2use crate::https::extractors::{DomainInfo, VerifiedClientInformation};
3use crate::https::middleware::KOpId;
4use crate::https::views::errors::HtmxError;
5use crate::https::views::navbar::NavbarCtx;
6use crate::https::views::Urls;
7use crate::https::ServerState;
8use askama::Template;
9use axum::extract::{Path, State};
10use axum::http::Uri;
11use axum::response::{IntoResponse, Response};
12use axum::Extension;
13use axum_htmx::{HxPushUrl, HxRequest};
14use kanidm_proto::attribute::Attribute;
15use kanidm_proto::internal::{OperationError, UserAuthToken};
16use kanidm_proto::scim_v1::server::{
17    ScimEffectiveAccess, ScimEntryKanidm, ScimListResponse, ScimPerson,
18};
19use kanidm_proto::scim_v1::ScimEntryGetQuery;
20use kanidm_proto::scim_v1::ScimFilter;
21use kanidmd_lib::constants::EntryClass;
22use kanidmd_lib::idm::ClientAuthInfo;
23use std::str::FromStr;
24use uuid::Uuid;
25
26pub const PERSON_ATTRIBUTES: [Attribute; 9] = [
27    Attribute::Uuid,
28    Attribute::Description,
29    Attribute::Name,
30    Attribute::DisplayName,
31    Attribute::Spn,
32    Attribute::Mail,
33    Attribute::Class,
34    Attribute::EntryManagedBy,
35    Attribute::DirectMemberOf,
36];
37
38#[derive(Template)]
39#[template(path = "admin/admin_panel_template.html")]
40pub(crate) struct PersonsView {
41    navbar_ctx: NavbarCtx,
42    partial: PersonsPartialView,
43}
44
45#[derive(Template)]
46#[template(path = "admin/admin_persons_partial.html")]
47struct PersonsPartialView {
48    persons: Vec<(ScimPerson, ScimEffectiveAccess)>,
49}
50
51#[derive(Template)]
52#[template(path = "admin/admin_panel_template.html")]
53struct PersonView {
54    partial: PersonViewPartial,
55    navbar_ctx: NavbarCtx,
56}
57
58#[derive(Template)]
59#[template(path = "admin/admin_person_view_partial.html")]
60struct PersonViewPartial {
61    person: ScimPerson,
62    scim_effective_access: ScimEffectiveAccess,
63}
64
65pub(crate) async fn view_person_view_get(
66    State(state): State<ServerState>,
67    HxRequest(is_htmx): HxRequest,
68    Extension(kopid): Extension<KOpId>,
69    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
70    Path(uuid): Path<Uuid>,
71    DomainInfo(domain_info): DomainInfo,
72) -> axum::response::Result<Response> {
73    let (person, scim_effective_access) =
74        get_person_info(uuid, state, &kopid, client_auth_info.clone()).await?;
75    let person_partial = PersonViewPartial {
76        person,
77        scim_effective_access,
78    };
79    let uat: &UserAuthToken = client_auth_info
80        .pre_validated_uat()
81        .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
82
83    let path_string = format!("/ui/admin/person/{uuid}/view");
84    let uri = Uri::from_str(path_string.as_str())
85        .map_err(|_| HtmxError::new(&kopid, OperationError::Backend, domain_info.clone()))?;
86    let push_url = HxPushUrl(uri);
87    Ok(if is_htmx {
88        (push_url, person_partial).into_response()
89    } else {
90        (
91            push_url,
92            PersonView {
93                partial: person_partial,
94                navbar_ctx: NavbarCtx::new(domain_info, &uat.ui_hints),
95            },
96        )
97            .into_response()
98    })
99}
100
101pub(crate) async fn view_persons_get(
102    State(state): State<ServerState>,
103    HxRequest(is_htmx): HxRequest,
104    Extension(kopid): Extension<KOpId>,
105    DomainInfo(domain_info): DomainInfo,
106    VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
107) -> axum::response::Result<Response> {
108    let persons = get_persons_info(state, &kopid, client_auth_info.clone()).await?;
109    let persons_partial = PersonsPartialView { persons };
110    let uat: &UserAuthToken = client_auth_info
111        .pre_validated_uat()
112        .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
113    let push_url = HxPushUrl(Uri::from_static("/ui/admin/persons"));
114    Ok(if is_htmx {
115        (push_url, persons_partial).into_response()
116    } else {
117        (
118            push_url,
119            PersonsView {
120                navbar_ctx: NavbarCtx::new(domain_info, &uat.ui_hints),
121                partial: persons_partial,
122            },
123        )
124            .into_response()
125    })
126}
127
128pub async fn get_person_info(
129    uuid: Uuid,
130    state: ServerState,
131    kopid: &KOpId,
132    client_auth_info: ClientAuthInfo,
133) -> Result<(ScimPerson, ScimEffectiveAccess), WebError> {
134    let scim_entry: ScimEntryKanidm = state
135        .qe_r_ref
136        .scim_entry_id_get(
137            client_auth_info.clone(),
138            kopid.eventid,
139            uuid.to_string(),
140            EntryClass::Person,
141            ScimEntryGetQuery {
142                attributes: Some(Vec::from(PERSON_ATTRIBUTES)),
143                ext_access_check: true,
144                ..Default::default()
145            },
146        )
147        .await?;
148
149    if let Some(personinfo_info) = scimentry_into_personinfo(scim_entry) {
150        Ok(personinfo_info)
151    } else {
152        Err(WebError::from(OperationError::InvalidState))
153    }
154}
155
156async fn get_persons_info(
157    state: ServerState,
158    kopid: &KOpId,
159    client_auth_info: ClientAuthInfo,
160) -> Result<Vec<(ScimPerson, ScimEffectiveAccess)>, WebError> {
161    let filter = ScimFilter::Equal(Attribute::Class.into(), EntryClass::Person.into());
162
163    let base: ScimListResponse = state
164        .qe_r_ref
165        .scim_entry_search(
166            client_auth_info.clone(),
167            kopid.eventid,
168            filter,
169            ScimEntryGetQuery {
170                attributes: Some(Vec::from(PERSON_ATTRIBUTES)),
171                ext_access_check: true,
172                sort_by: Some(Attribute::Name),
173                ..Default::default()
174            },
175        )
176        .await?;
177
178    let persons: Vec<_> = base
179        .resources
180        .into_iter()
181        // TODO: Filtering away unsuccessful entries may not be desired.
182        .filter_map(scimentry_into_personinfo)
183        .collect();
184
185    Ok(persons)
186}
187
188fn scimentry_into_personinfo(
189    scim_entry: ScimEntryKanidm,
190) -> Option<(ScimPerson, ScimEffectiveAccess)> {
191    let scim_effective_access = scim_entry.ext_access_check.clone()?; // TODO: This should be an error msg.
192    let person = ScimPerson::try_from(scim_entry).ok()?;
193
194    Some((person, scim_effective_access))
195}