From 8105ed9e2310ad7e368bbcaab66d4a5570d4d929 Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 25 Mar 2025 18:15:21 +0100 Subject: [PATCH] Add sso identifier in admin user panel --- src/api/admin.rs | 31 ++++++++++++++++++++++++-- src/db/models/event.rs | 2 +- src/db/models/user.rs | 19 ++++++++++++++-- src/static/scripts/admin.css | 4 ++-- src/static/scripts/admin.js | 14 +++++++++--- src/static/scripts/admin_users.js | 33 +++++++++++++++++++++++++--- src/static/templates/admin/users.hbs | 11 ++++++++++ 7 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index badc6383..1b33033a 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -46,6 +46,7 @@ pub fn routes() -> Vec { invite_user, logout, delete_user, + delete_sso_user, deauth_user, disable_user, enable_user, @@ -239,6 +240,7 @@ struct AdminTemplateData { page_data: Option, logged_in: bool, urlpath: String, + sso_enabled: bool, } impl AdminTemplateData { @@ -248,6 +250,7 @@ impl AdminTemplateData { page_data: Some(page_data), logged_in: true, urlpath: CONFIG.domain_path(), + sso_enabled: CONFIG.sso_enabled(), } } @@ -336,7 +339,7 @@ fn logout(cookies: &CookieJar<'_>) -> Redirect { async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json { let users = User::get_all(&mut conn).await; let mut users_json = Vec::with_capacity(users.len()); - for u in users { + for (u, _) in users { let mut usr = u.to_json(&mut conn).await; usr["userEnabled"] = json!(u.enabled); usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); @@ -354,7 +357,7 @@ async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json { async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult> { let users = User::get_all(&mut conn).await; let mut users_json = Vec::with_capacity(users.len()); - for u in users { + for (u, sso_u) in users { let mut usr = u.to_json(&mut conn).await; usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &mut conn).await); usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &mut conn).await); @@ -365,6 +368,9 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult json!(format_naive_datetime_local(&dt, DT_FMT)), None => json!("Never"), }; + + usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new())); + users_json.push(usr); } @@ -417,6 +423,27 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em res } +#[delete("/users//sso", format = "application/json")] +async fn delete_sso_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult { + let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await; + let res = SsoUser::delete(&user_id, &mut conn).await; + + for membership in memberships { + log_event( + EventType::OrganizationUserUnlinkedSso as i32, + &membership.uuid, + &membership.org_uuid, + &ACTING_ADMIN_USER.into(), + 14, // Use UnknownBrowser type + &token.ip.ip, + &mut conn, + ) + .await; + } + + res +} + #[post("/users//deauth", format = "application/json")] async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&user_id, &mut conn).await?; diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 985eca7e..49968a99 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -90,7 +90,7 @@ pub enum EventType { OrganizationUserUpdated = 1502, OrganizationUserRemoved = 1503, OrganizationUserUpdatedGroups = 1504, - // OrganizationUserUnlinkedSso = 1505, // Not supported + OrganizationUserUnlinkedSso = 1505, // Not supported OrganizationUserResetPasswordEnroll = 1506, OrganizationUserResetPasswordWithdraw = 1507, OrganizationUserAdminResetPassword = 1508, diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 5e7922e0..f14a986a 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -394,9 +394,16 @@ impl User { }} } - pub async fn get_all(conn: &mut DbConn) -> Vec { + pub async fn get_all(conn: &mut DbConn) -> Vec<(User, Option)> { db_run! {conn: { - users::table.load::(conn).expect("Error loading users").from_db() + users::table + .left_join(sso_users::table) + .select(<(UserDb, Option)>::as_select()) + .load(conn) + .expect("Error loading groups for user") + .into_iter() + .map(|(user, sso_user)| { (user.from_db(), sso_user.from_db()) }) + .collect() }} } @@ -532,4 +539,12 @@ impl SsoUser { .map(|(user, sso_user)| { (user.from_db(), sso_user.from_db()) }) }} } + + pub async fn delete(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { + db_run! {conn: { + diesel::delete(sso_users::table.filter(sso_users::user_uuid.eq(user_uuid))) + .execute(conn) + .map_res("Error deleting sso user") + }} + } } diff --git a/src/static/scripts/admin.css b/src/static/scripts/admin.css index ee035ac4..44448d6a 100644 --- a/src/static/scripts/admin.css +++ b/src/static/scripts/admin.css @@ -38,8 +38,8 @@ img { max-width: 130px; } #users-table .vw-actions, #orgs-table .vw-actions { - min-width: 135px; - max-width: 140px; + min-width: 155px; + max-width: 160px; } #users-table .vw-org-cell { max-height: 120px; diff --git a/src/static/scripts/admin.js b/src/static/scripts/admin.js index b194a91d..06d6ca5c 100644 --- a/src/static/scripts/admin.js +++ b/src/static/scripts/admin.js @@ -28,11 +28,11 @@ function msg(text, reload_page = true) { reload_page && reload(); } -function _post(url, successMsg, errMsg, body, reload_page = true) { +function _fetch(method, url, successMsg, errMsg, body, reload_page = true) { let respStatus; let respStatusText; fetch(url, { - method: "POST", + method: method, body: body, mode: "same-origin", credentials: "same-origin", @@ -65,6 +65,14 @@ function _post(url, successMsg, errMsg, body, reload_page = true) { }); } +function _post(url, successMsg, errMsg, body, reload_page = true) { + return _fetch("POST", url, successMsg, errMsg, body, reload_page); +} + +function _delete(url, successMsg, errMsg, body, reload_page = true) { + return _fetch("DELETE", url, successMsg, errMsg, body, reload_page); +} + // Bootstrap Theme Selector const getStoredTheme = () => localStorage.getItem("theme"); const setStoredTheme = theme => localStorage.setItem("theme", theme); @@ -146,4 +154,4 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { navItem[0].className = navItem[0].className + " active"; navItem[0].setAttribute("aria-current", "page"); } -}); \ No newline at end of file +}); diff --git a/src/static/scripts/admin_users.js b/src/static/scripts/admin_users.js index 54fdedf2..71015d1d 100644 --- a/src/static/scripts/admin_users.js +++ b/src/static/scripts/admin_users.js @@ -24,6 +24,28 @@ function deleteUser(event) { } } +function deleteSSOUser(event) { + event.preventDefault(); + event.stopPropagation(); + const id = event.target.parentNode.dataset.vwUserUuid; + const email = event.target.parentNode.dataset.vwUserEmail; + if (!id || !email) { + alert("Required parameters not found!"); + return false; + } + const input_email = prompt(`To delete user "${email}", please type the email below`); + if (input_email != null) { + if (input_email == email) { + _delete(`${BASE_URL}/admin/users/${id}/sso`, + "User SSO Associtation deleted correctly", + "Error deleting user SSO association" + ); + } else { + alert("Wrong email, please try again"); + } + } +} + function remove2fa(event) { event.preventDefault(); event.stopPropagation(); @@ -246,6 +268,9 @@ function initUserTable() { document.querySelectorAll("button[vw-delete-user]").forEach(btn => { btn.addEventListener("click", deleteUser); }); + document.querySelectorAll("button[vw-delete-sso-user]").forEach(btn => { + btn.addEventListener("click", deleteSSOUser); + }); document.querySelectorAll("button[vw-disable-user]").forEach(btn => { btn.addEventListener("click", disableUser); }); @@ -263,6 +288,8 @@ function initUserTable() { // onLoad events document.addEventListener("DOMContentLoaded", (/*event*/) => { + const size = jQuery("#users-table > thead th").length; + const ssoOffset = size-7; jQuery("#users-table").DataTable({ "drawCallback": function() { initUserTable(); @@ -275,10 +302,10 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { ], "pageLength": -1, // Default show all "columnDefs": [{ - "targets": [1, 2], + "targets": [1 + ssoOffset, 2 + ssoOffset], "type": "date-iso" }, { - "targets": 6, + "targets": size-1, "searchable": false, "orderable": false }] @@ -303,4 +330,4 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { if (btnInviteUserForm) { btnInviteUserForm.addEventListener("submit", inviteUser); } -}); \ No newline at end of file +}); diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 65470525..a6a82765 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -6,6 +6,9 @@ User + {{#if sso_enabled}} + SSO Identifier + {{/if}} Created at Last Active Entries @@ -38,6 +41,11 @@ + {{#if ../sso_enabled}} + + {{sso_identifier}} + + {{/if}} {{created_at}} @@ -67,6 +75,9 @@ {{/if}}

+ {{#if ../sso_enabled}} +
+ {{/if}} {{#if user_enabled}}
{{else}}