kanidmd_core/https/views/admin/
groups.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_extra::extract::Form;
14use axum_htmx::{HxPushUrl, HxRequest};
15use futures_util::TryFutureExt;
16use kanidm_proto::attribute::Attribute;
17use kanidm_proto::internal::{OperationError, UserAuthToken};
18use kanidm_proto::scim_v1::server::{
19 ScimEffectiveAccess, ScimEntryKanidm, ScimGroup, ScimListResponse, ScimValueKanidm,
20};
21use kanidm_proto::scim_v1::ScimEntryGetQuery;
22use kanidm_proto::scim_v1::{client::ScimEntryPutKanidm, ScimFilter};
23use kanidmd_lib::constants::EntryClass;
24use kanidmd_lib::idm::ClientAuthInfo;
25use serde::{Deserialize, Serialize};
26use std::collections::BTreeMap;
27use std::str::FromStr;
28use uuid::Uuid;
29
30pub const GROUP_ATTRIBUTES: [Attribute; 3] =
31 [Attribute::Uuid, Attribute::Name, Attribute::Description];
32
33#[derive(Template)]
34#[template(path = "admin/admin_panel_template.html")]
35pub(crate) struct GroupsView {
36 navbar_ctx: NavbarCtx,
37 partial: GroupsPartialView,
38}
39
40#[derive(Template)]
41#[template(path = "admin/admin_groups_partial.html")]
42struct GroupsPartialView {
43 groups: Vec<(ScimGroup, ScimEffectiveAccess)>,
44}
45
46#[derive(Template)]
47#[template(path = "admin/admin_panel_template.html")]
48struct GroupView {
49 partial: GroupViewPartial,
50 navbar_ctx: NavbarCtx,
51}
52
53#[derive(Template)]
54#[template(path = "admin/admin_group_view_partial.html")]
55struct GroupViewPartial {
56 group: ScimGroup,
57 can_rw: bool,
58 scim_effective_access: ScimEffectiveAccess,
59}
60
61#[derive(Template)]
62#[template(path = "admin/saved_toast.html")]
63struct SavedToast {}
64
65pub(crate) async fn view_group_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 (group, scim_effective_access) =
74 get_group_info(uuid, state.clone(), &kopid, client_auth_info.clone()).await?;
75 let uat: &UserAuthToken = client_auth_info
76 .pre_validated_uat()
77 .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))?;
78
79 let time = time::OffsetDateTime::now_utc() + time::Duration::new(60, 0);
80 let can_rw = uat.purpose_readwrite_active(time);
81 let group_partial = GroupViewPartial {
82 group,
83 can_rw,
84 scim_effective_access,
85 };
86
87 let path_string = format!("/ui/admin/group/{uuid}/view");
88 let uri = Uri::from_str(path_string.as_str())
89 .map_err(|_| HtmxError::new(&kopid, OperationError::Backend, domain_info.clone()))?;
90 let push_url = HxPushUrl(uri);
91 Ok(if is_htmx {
92 (push_url, group_partial).into_response()
93 } else {
94 (
95 push_url,
96 GroupView {
97 partial: group_partial,
98 navbar_ctx: NavbarCtx { domain_info },
99 },
100 )
101 .into_response()
102 })
103}
104
105pub(crate) async fn view_groups_get(
106 State(state): State<ServerState>,
107 HxRequest(is_htmx): HxRequest,
108 Extension(kopid): Extension<KOpId>,
109 DomainInfo(domain_info): DomainInfo,
110 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
111) -> axum::response::Result<Response> {
112 let groups = get_groups_info(state, &kopid, client_auth_info).await?;
113 let groups_partial = GroupsPartialView { groups };
114
115 let push_url = HxPushUrl(Uri::from_static("/ui/admin/groups"));
116 Ok(if is_htmx {
117 (push_url, groups_partial).into_response()
118 } else {
119 (
120 push_url,
121 GroupsView {
122 navbar_ctx: NavbarCtx { domain_info },
123 partial: groups_partial,
124 },
125 )
126 .into_response()
127 })
128}
129
130pub async fn get_group_info(
131 uuid: Uuid,
132 state: ServerState,
133 kopid: &KOpId,
134 client_auth_info: ClientAuthInfo,
135) -> Result<(ScimGroup, ScimEffectiveAccess), WebError> {
136 let scim_entry: ScimEntryKanidm = state
137 .qe_r_ref
138 .scim_entry_id_get(
139 client_auth_info.clone(),
140 kopid.eventid,
141 uuid.to_string(),
142 EntryClass::Group,
143 ScimEntryGetQuery {
144 attributes: Some(Vec::from(GROUP_ATTRIBUTES)),
145 ext_access_check: true,
146 ..Default::default()
147 },
148 )
149 .await?;
150
151 if let Some(groupinfo_info) = scimentry_into_groupinfo(scim_entry) {
152 Ok(groupinfo_info)
153 } else {
154 Err(WebError::from(OperationError::InvalidState))
155 }
156}
157
158async fn get_groups_info(
159 state: ServerState,
160 kopid: &KOpId,
161 client_auth_info: ClientAuthInfo,
162) -> Result<Vec<(ScimGroup, ScimEffectiveAccess)>, WebError> {
163 let filter = ScimFilter::Equal(Attribute::Class.into(), EntryClass::Group.into());
164
165 let base: ScimListResponse = state
166 .qe_r_ref
167 .scim_entry_search(
168 client_auth_info.clone(),
169 kopid.eventid,
170 filter,
171 ScimEntryGetQuery {
172 attributes: Some(Vec::from(GROUP_ATTRIBUTES)),
173 ext_access_check: true,
174 sort_by: Some(Attribute::Name),
175 ..Default::default()
176 },
177 )
178 .await?;
179
180 let groups: Vec<_> = base
181 .resources
182 .into_iter()
183 .filter_map(scimentry_into_groupinfo)
185 .collect();
186
187 Ok(groups)
188}
189
190#[derive(Clone, Debug, Serialize, Deserialize)]
191pub(crate) struct SaveGroupForm {
192 #[serde(rename = "name")]
193 account_name: String,
194 description: Option<String>,
195}
196
197pub(crate) async fn edit_group(
198 State(state): State<ServerState>,
199 Extension(kopid): Extension<KOpId>,
200 DomainInfo(domain_info): DomainInfo,
201 VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
202 Path(group_uuid): Path<Uuid>,
203 Form(query): Form<SaveGroupForm>,
205) -> axum::response::Result<Response> {
206 let mut attrs = BTreeMap::new();
207 attrs.insert(
208 Attribute::Name,
209 Some(ScimValueKanidm::String(query.account_name)),
210 );
211
212 let (group_info, _) =
213 get_group_info(group_uuid, state.clone(), &kopid, client_auth_info.clone()).await?;
214
215 if group_info.description != query.description {
219 attrs.insert(
220 Attribute::Description,
221 query.description.map(ScimValueKanidm::String),
222 );
223 }
224
225 let generic = ScimEntryPutKanidm {
226 id: group_uuid,
227 attrs,
228 }
229 .try_into()
230 .map_err(|_| HtmxError::new(&kopid, OperationError::Backend, domain_info.clone()))?;
231
232 state
233 .qe_w_ref
234 .handle_scim_entry_put(client_auth_info.clone(), kopid.eventid, generic)
235 .map_err(|op_err| HtmxError::new(&kopid, op_err, domain_info.clone()))
236 .await?;
237
238 Ok((SavedToast {}).into_response())
240}
241
242fn scimentry_into_groupinfo(
243 scim_entry: ScimEntryKanidm,
244) -> Option<(ScimGroup, ScimEffectiveAccess)> {
245 let scim_effective_access = scim_entry.ext_access_check.clone()?; let group = ScimGroup::try_from(scim_entry).ok()?;
247
248 Some((group, scim_effective_access))
249}