diff --git a/proto/src/scim_v1/server.rs b/proto/src/scim_v1/server.rs index 8a0d3efe0..9b524d686 100644 --- a/proto/src/scim_v1/server.rs +++ b/proto/src/scim_v1/server.rs @@ -13,6 +13,7 @@ use tracing::debug; use url::Url; use utoipa::ToSchema; use uuid::Uuid; +use crate::v1::AccountType; /// A strongly typed ScimEntry that is for transmission to clients. This uses /// Kanidm internal strong types for values allowing direct serialisation and @@ -258,7 +259,76 @@ pub enum ScimValueKanidm { UiHints(Vec<UiHint>), } +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)] +pub struct ScimPerson { + pub uuid: Uuid, + pub account_type: AccountType, + pub name: String, + pub displayname: Option<String>, + pub spn: String, + pub description: Option<String>, + pub mails: Vec<ScimMail>, + pub managed_by: Option<ScimReference>, + pub groups: Vec<ScimReference> +} + +impl TryFrom<ScimEntryKanidm> for ScimPerson { + type Error = (); + + fn try_from(scim_entry: ScimEntryKanidm) -> Result<Self, Self::Error> { + let uuid = scim_entry.header.id; + scim_entry.account_type(); + let name = scim_entry.attr_str(&Attribute::Name).ok_or(())?.to_string(); + let displayname = scim_entry + .attr_str(&Attribute::DisplayName) + .map(|s| s.to_string()); + let spn = scim_entry.attr_str(&Attribute::Spn).ok_or(())?.to_string(); + let description = scim_entry + .attr_str(&Attribute::Description) + .map(|t| t.to_string()); + let mails = scim_entry.attr_mails().cloned().unwrap_or_default(); + let groups = scim_entry.attr_references(&Attribute::DirectMemberOf).cloned().unwrap_or(vec![]); + + let managed_by = scim_entry.attr_reference(&Attribute::EntryManagedBy).cloned(); + + Ok(ScimPerson { + uuid, + account_type: AccountType::Person, + name, + displayname, + spn, + description, + mails, + managed_by, + groups, + }) + } +} + impl ScimEntryKanidm { + pub fn account_type(&self) -> Option<AccountType> { + match self.attrs.get(&Attribute::Class) { + Some(ScimValueKanidm::ArrayString(classes)) => { + if classes.contains(&format!("{}", AccountType::Person)) { + Some(AccountType::Person) + } else if classes.contains(&format!("{}", AccountType::ServiceAccount)) { + Some(AccountType::ServiceAccount) + } else { + None + } + }, + Some(sv) => { + debug!("SCIM entry had the Attribute::Class attribute but it was not a ScimValueKanidm::ArrayString type, actual: {:?}", sv); + None + } + None => { + debug!("SCIM entry with no classes ??"); + None + }, + } + } + pub fn attr_str(&self, attr: &Attribute) -> Option<&str> { match self.attrs.get(attr) { Some(ScimValueKanidm::String(inner_string)) => Some(inner_string.as_str()), @@ -292,6 +362,17 @@ impl ScimEntryKanidm { } } + pub fn attr_reference(&self, attr: &Attribute) -> Option<&ScimReference> { + match self.attrs.get(attr) { + Some(ScimValueKanidm::EntryReference(refer)) => Some(refer), + Some(sv) => { + debug!("SCIM entry had the {} attribute but it was not a ScimValueKanidm::ScimReference type, actual: {:?}", attr, sv); + None + } + None => None, + } + } + pub fn attr_references(&self, attr: &Attribute) -> Option<&Vec<ScimReference>> { match self.attrs.get(attr) { Some(ScimValueKanidm::EntryReferences(refs)) => Some(refs), diff --git a/proto/src/v1/mod.rs b/proto/src/v1/mod.rs index f30c694e2..d94cde2a7 100644 --- a/proto/src/v1/mod.rs +++ b/proto/src/v1/mod.rs @@ -19,7 +19,7 @@ pub use self::auth::*; pub use self::unix::*; /// The type of Account in use. -#[derive(Clone, Copy, Debug, ToSchema)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, ToSchema)] pub enum AccountType { Person, ServiceAccount, diff --git a/server/core/src/https/views/admin/accounts.rs b/server/core/src/https/views/admin/accounts.rs index 27fb62677..51c53b484 100644 --- a/server/core/src/https/views/admin/accounts.rs +++ b/server/core/src/https/views/admin/accounts.rs @@ -13,17 +13,28 @@ use axum_htmx::{HxPushUrl, HxRequest}; use futures_util::TryFutureExt; use kanidm_proto::attribute::Attribute; use kanidm_proto::internal::OperationError; -use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimEntryKanidm}; -use kanidm_proto::scim_v1::{ScimEntryGetQuery, ScimMail}; +use kanidm_proto::scim_v1::server::{ScimEffectiveAccess, ScimEntryKanidm, ScimPerson}; +use kanidm_proto::scim_v1::ScimEntryGetQuery; use kanidmd_lib::constants::EntryClass; use kanidmd_lib::filter::{f_and, f_eq, Filter, FC}; use kanidmd_lib::idm::server::DomainInfoRead; use kanidmd_lib::idm::ClientAuthInfo; -use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::str::FromStr; use uuid::Uuid; +const ACCOUNT_ATTRIBUTES: [Attribute; 9] = [ + Attribute::Uuid, + Attribute::Description, + Attribute::Name, + Attribute::DisplayName, + Attribute::Spn, + Attribute::Mail, + Attribute::Class, + Attribute::EntryManagedBy, + Attribute::DirectMemberOf +]; + #[derive(Template)] #[template(path = "admin/admin_panel_template.html")] pub(crate) struct AccountsView { @@ -34,18 +45,7 @@ pub(crate) struct AccountsView { #[derive(Template)] #[template(path = "admin/admin_accounts_partial.html")] struct AccountsPartialView { - accounts: Vec<AccountInfo>, -} - -#[derive(Serialize, Deserialize, Debug)] -struct AccountInfo { - uuid: Uuid, - name: String, - displayname: Option<String>, - spn: String, - description: Option<String>, - mails: Vec<ScimMail>, - scim_effective_access: ScimEffectiveAccess, + accounts: Vec<(ScimPerson, ScimEffectiveAccess)>, } #[derive(Template)] @@ -58,7 +58,8 @@ struct AccountView { #[derive(Template)] #[template(path = "admin/admin_account_view_partial.html")] struct AccountViewPartial { - account: AccountInfo, + account: ScimPerson, + scim_effective_access: ScimEffectiveAccess, } pub(crate) async fn view_account_view_get( @@ -69,9 +70,12 @@ pub(crate) async fn view_account_view_get( Path(uuid): Path<Uuid>, DomainInfo(domain_info): DomainInfo, ) -> axum::response::Result<Response> { - let account = + let (account, scim_effective_access) = get_account_info(uuid, state, &kopid, client_auth_info, domain_info.clone()).await?; - let accounts_partial = AccountViewPartial { account }; + let accounts_partial = AccountViewPartial { + account, + scim_effective_access, + }; let path_string = format!("/ui/admin/account/{uuid}/view"); let uri = Uri::from_str(path_string.as_str()) @@ -122,7 +126,7 @@ async fn get_account_info( kopid: &KOpId, client_auth_info: ClientAuthInfo, domain_info: DomainInfoRead, -) -> Result<AccountInfo, ErrorResponse> { +) -> Result<(ScimPerson, ScimEffectiveAccess), ErrorResponse> { let scim_entry: ScimEntryKanidm = state .qe_r_ref .scim_entry_id_get( @@ -131,14 +135,7 @@ async fn get_account_info( uuid.to_string(), EntryClass::Account, ScimEntryGetQuery { - attributes: Some(vec![ - Attribute::Uuid, - Attribute::Description, - Attribute::Name, - Attribute::DisplayName, - Attribute::Spn, - Attribute::Mail, - ]), + attributes: Some(Vec::from(ACCOUNT_ATTRIBUTES)), ext_access_check: true, }, ) @@ -157,16 +154,9 @@ async fn get_accounts_info( kopid: &KOpId, client_auth_info: ClientAuthInfo, domain_info: DomainInfoRead, -) -> Result<Vec<AccountInfo>, ErrorResponse> { +) -> Result<Vec<(ScimPerson, ScimEffectiveAccess)>, ErrorResponse> { let filter = filter_all!(f_and!([f_eq(Attribute::Class, EntryClass::Account.into())])); - let attrs = Some(BTreeSet::from([ - Attribute::Uuid, - Attribute::Description, - Attribute::Name, - Attribute::DisplayName, - Attribute::Spn, - Attribute::Mail, - ])); + let attrs = Some(BTreeSet::from(ACCOUNT_ATTRIBUTES)); let base: Vec<ScimEntryKanidm> = state .qe_r_ref .scim_entry_search(client_auth_info.clone(), filter, kopid.eventid, attrs, true) @@ -180,33 +170,17 @@ async fn get_accounts_info( .filter_map(scimentry_into_accountinfo) .collect(); - accounts.sort_by_key(|gi| gi.uuid); + accounts.sort_by_key(|(sp, _)| sp.uuid); accounts.reverse(); Ok(accounts) } -fn scimentry_into_accountinfo(scim_entry: ScimEntryKanidm) -> Option<AccountInfo> { - let uuid = scim_entry.header.id; - let name = scim_entry.attr_str(&Attribute::Name)?.to_string(); - let displayname = scim_entry - .attr_str(&Attribute::DisplayName) - .map(|s| s.to_string()); - let spn = scim_entry.attr_str(&Attribute::Spn)?.to_string(); - let description = scim_entry - .attr_str(&Attribute::Description) - .map(|t| t.to_string()); - let mails = scim_entry.attr_mails().cloned().unwrap_or_default(); +fn scimentry_into_accountinfo(scim_entry: ScimEntryKanidm) -> Option<(ScimPerson, ScimEffectiveAccess)> { + let scim_effective_access = scim_entry.ext_access_check.clone()?; // TODO: This should be an error msg. + let account = ScimPerson::try_from(scim_entry).ok()?; - let option = scim_entry.ext_access_check; - let scim_effective_access = option?; // TODO: This should be an error msg. - - Some(AccountInfo { - scim_effective_access, - uuid, - name, - displayname, - spn, - description, - mails, - }) + Some(( + account, + scim_effective_access + )) } diff --git a/server/core/templates/admin/admin_account_details_partial.html b/server/core/templates/admin/admin_account_details_partial.html index 9c191d505..568f57ead 100644 --- a/server/core/templates/admin/admin_account_details_partial.html +++ b/server/core/templates/admin/admin_account_details_partial.html @@ -1,5 +1,5 @@ (% macro string_attr(dispname, name, value, editable, attribute) %) -(% if account.scim_effective_access.search.check(attribute|as_ref) %) +(% if scim_effective_access.search.check(attribute|as_ref) %) <div class="row mt-3"> <label for="account(( name ))" class="col-12 col-md-3 col-lg-2 col-form-label fw-bold py-0">(( dispname ))</label> <div class="col-12 col-md-8 col-lg-6"> @@ -25,4 +25,10 @@ (% else %) (% call string_attr("Description", "description", "none", true, Attribute::Description) %) (% endif %) + + (% if let Some(entry_managed_by) = account.managed_by %) + (% call string_attr("Managed By", "managed_by", entry_managed_by.value, true, Attribute::EntryManagedBy) %) + (% else %) + (% call string_attr("Managed By", "managed_by", "none", true, Attribute::EntryManagedBy) %) + (% endif %) </form> \ No newline at end of file diff --git a/server/core/templates/admin/admin_account_view_partial.html b/server/core/templates/admin/admin_account_view_partial.html index 5b8513140..13ac46e02 100644 --- a/server/core/templates/admin/admin_account_view_partial.html +++ b/server/core/templates/admin/admin_account_view_partial.html @@ -2,19 +2,19 @@ (% block accounts_item_extra_classes %)active(% endblock %) -(% block selected_admin_page %) -Accounts Management -(% endblock %) - - (% block admin_page %) -<h4><a href="/ui/admin/accounts" hx-target="#main">🠨</a>Viewing Account details</h4> +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="/ui/admin/accounts" hx-target="#main">Accounts Management</a></li> + <li class="breadcrumb-item active" aria-current="page">Viewing</li> + </ol> +</nav> (% include "admin_account_details_partial.html" %) <hr> -(% if account.scim_effective_access.search.check(Attribute::Mail|as_ref) %) +(% if scim_effective_access.search.check(Attribute::Mail|as_ref) %) <label class="mt-3 fw-bold">Emails</label> <form hx-validate="true" hx-ext="bs-validation"> (% if account.mails.len() == 0 %) @@ -33,4 +33,25 @@ Accounts Management </form> (% endif %) +(% if scim_effective_access.search.check(Attribute::DirectMemberOf|as_ref) %) +<label class="mt-3 fw-bold">DirectMemberOf</label> +<form hx-validate="true" hx-ext="bs-validation"> + (% if account.groups.len() == 0 %) + <p>There are no groups this account is a direct member of.</p> + (% else %) + <ol class="list-group col-12 col-md-8 col-lg-6"> + (% for group in account.groups %) + <li id="accountGroup(( loop.index ))" class="list-group-item d-flex flex-row justify-content-between"> + <div class="d-flex align-items-center">(( group.value ))</div> + <div class="buttons float-end"> + </div> + </li> + (% endfor %) + </ol> + (% endif %) +</form> +(% endif %) + + + (% endblock %) \ No newline at end of file diff --git a/server/core/templates/admin/admin_accounts_partial.html b/server/core/templates/admin/admin_accounts_partial.html index 60330f15a..71793016a 100644 --- a/server/core/templates/admin/admin_accounts_partial.html +++ b/server/core/templates/admin/admin_accounts_partial.html @@ -2,13 +2,15 @@ (% block accounts_item_extra_classes %)active(% endblock %) -(% block selected_admin_page %) -Account Management -(% endblock %) - (% block admin_page %) +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active" aria-current="page">Accounts Management</li> + </ol> +</nav> + <ul class="list-group"> - (% for account in accounts %) + (% for (account, _) in accounts %) <li class="list-group-item d-flex flex-row justify-content-between"> <div class="d-flex align-items-center"> <a href="/ui/admin/account/(( account.uuid ))/view" hx-target="#main">(( account.name ))</a> <span class="text-secondary d-none d-lg-inline-block mx-4">(( account.uuid ))</span> diff --git a/server/core/templates/admin/admin_partial_base.html b/server/core/templates/admin/admin_partial_base.html index 9cadb6073..7271de0fa 100644 --- a/server/core/templates/admin/admin_partial_base.html +++ b/server/core/templates/admin/admin_partial_base.html @@ -12,10 +12,6 @@ Oauth2 (placeholder)</a> </div> <div id="settings-window" class="flex-grow-1 ps-sm-4 pt-sm-0 pt-4"> - <div> - <h2>(% block selected_admin_page %)(% endblock %)</h2> - </div> - (% block admin_page %) (% endblock %) </div>