Browse Source

Add sso identifier in admin user panel

pull/3899/head
Timshel 1 month ago
parent
commit
8105ed9e23
  1. 31
      src/api/admin.rs
  2. 2
      src/db/models/event.rs
  3. 19
      src/db/models/user.rs
  4. 4
      src/static/scripts/admin.css
  5. 14
      src/static/scripts/admin.js
  6. 33
      src/static/scripts/admin_users.js
  7. 11
      src/static/templates/admin/users.hbs

31
src/api/admin.rs

@ -46,6 +46,7 @@ pub fn routes() -> Vec<Route> {
invite_user, invite_user,
logout, logout,
delete_user, delete_user,
delete_sso_user,
deauth_user, deauth_user,
disable_user, disable_user,
enable_user, enable_user,
@ -239,6 +240,7 @@ struct AdminTemplateData {
page_data: Option<Value>, page_data: Option<Value>,
logged_in: bool, logged_in: bool,
urlpath: String, urlpath: String,
sso_enabled: bool,
} }
impl AdminTemplateData { impl AdminTemplateData {
@ -248,6 +250,7 @@ impl AdminTemplateData {
page_data: Some(page_data), page_data: Some(page_data),
logged_in: true, logged_in: true,
urlpath: CONFIG.domain_path(), 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<Value> { async fn get_users_json(_token: AdminToken, mut conn: DbConn) -> Json<Value> {
let users = User::get_all(&mut conn).await; let users = User::get_all(&mut conn).await;
let mut users_json = Vec::with_capacity(users.len()); 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; let mut usr = u.to_json(&mut conn).await;
usr["userEnabled"] = json!(u.enabled); usr["userEnabled"] = json!(u.enabled);
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT)); 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<Value> {
async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> { async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult<Html<String>> {
let users = User::get_all(&mut conn).await; let users = User::get_all(&mut conn).await;
let mut users_json = Vec::with_capacity(users.len()); 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; let mut usr = u.to_json(&mut conn).await;
usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &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); 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<Html<
Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)), Some(dt) => json!(format_naive_datetime_local(&dt, DT_FMT)),
None => json!("Never"), None => json!("Never"),
}; };
usr["sso_identifier"] = json!(sso_u.map(|u| u.identifier.to_string()).unwrap_or(String::new()));
users_json.push(usr); users_json.push(usr);
} }
@ -417,6 +423,27 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
res res
} }
#[delete("/users/<user_id>/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/<user_id>/deauth", format = "application/json")] #[post("/users/<user_id>/deauth", format = "application/json")]
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { 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?; let mut user = get_user_or_404(&user_id, &mut conn).await?;

2
src/db/models/event.rs

@ -90,7 +90,7 @@ pub enum EventType {
OrganizationUserUpdated = 1502, OrganizationUserUpdated = 1502,
OrganizationUserRemoved = 1503, OrganizationUserRemoved = 1503,
OrganizationUserUpdatedGroups = 1504, OrganizationUserUpdatedGroups = 1504,
// OrganizationUserUnlinkedSso = 1505, // Not supported OrganizationUserUnlinkedSso = 1505, // Not supported
OrganizationUserResetPasswordEnroll = 1506, OrganizationUserResetPasswordEnroll = 1506,
OrganizationUserResetPasswordWithdraw = 1507, OrganizationUserResetPasswordWithdraw = 1507,
OrganizationUserAdminResetPassword = 1508, OrganizationUserAdminResetPassword = 1508,

19
src/db/models/user.rs

@ -394,9 +394,16 @@ impl User {
}} }}
} }
pub async fn get_all(conn: &mut DbConn) -> Vec<Self> { pub async fn get_all(conn: &mut DbConn) -> Vec<(User, Option<SsoUser>)> {
db_run! {conn: { db_run! {conn: {
users::table.load::<UserDb>(conn).expect("Error loading users").from_db() users::table
.left_join(sso_users::table)
.select(<(UserDb, Option<SsoUserDb>)>::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()) }) .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")
}}
}
} }

4
src/static/scripts/admin.css

@ -38,8 +38,8 @@ img {
max-width: 130px; max-width: 130px;
} }
#users-table .vw-actions, #orgs-table .vw-actions { #users-table .vw-actions, #orgs-table .vw-actions {
min-width: 135px; min-width: 155px;
max-width: 140px; max-width: 160px;
} }
#users-table .vw-org-cell { #users-table .vw-org-cell {
max-height: 120px; max-height: 120px;

14
src/static/scripts/admin.js

@ -28,11 +28,11 @@ function msg(text, reload_page = true) {
reload_page && reload(); 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 respStatus;
let respStatusText; let respStatusText;
fetch(url, { fetch(url, {
method: "POST", method: method,
body: body, body: body,
mode: "same-origin", mode: "same-origin",
credentials: "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 // Bootstrap Theme Selector
const getStoredTheme = () => localStorage.getItem("theme"); const getStoredTheme = () => localStorage.getItem("theme");
const setStoredTheme = theme => localStorage.setItem("theme", 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].className = navItem[0].className + " active";
navItem[0].setAttribute("aria-current", "page"); navItem[0].setAttribute("aria-current", "page");
} }
}); });

33
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) { function remove2fa(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -246,6 +268,9 @@ function initUserTable() {
document.querySelectorAll("button[vw-delete-user]").forEach(btn => { document.querySelectorAll("button[vw-delete-user]").forEach(btn => {
btn.addEventListener("click", deleteUser); 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 => { document.querySelectorAll("button[vw-disable-user]").forEach(btn => {
btn.addEventListener("click", disableUser); btn.addEventListener("click", disableUser);
}); });
@ -263,6 +288,8 @@ function initUserTable() {
// onLoad events // onLoad events
document.addEventListener("DOMContentLoaded", (/*event*/) => { document.addEventListener("DOMContentLoaded", (/*event*/) => {
const size = jQuery("#users-table > thead th").length;
const ssoOffset = size-7;
jQuery("#users-table").DataTable({ jQuery("#users-table").DataTable({
"drawCallback": function() { "drawCallback": function() {
initUserTable(); initUserTable();
@ -275,10 +302,10 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
], ],
"pageLength": -1, // Default show all "pageLength": -1, // Default show all
"columnDefs": [{ "columnDefs": [{
"targets": [1, 2], "targets": [1 + ssoOffset, 2 + ssoOffset],
"type": "date-iso" "type": "date-iso"
}, { }, {
"targets": 6, "targets": size-1,
"searchable": false, "searchable": false,
"orderable": false "orderable": false
}] }]
@ -303,4 +330,4 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
if (btnInviteUserForm) { if (btnInviteUserForm) {
btnInviteUserForm.addEventListener("submit", inviteUser); btnInviteUserForm.addEventListener("submit", inviteUser);
} }
}); });

11
src/static/templates/admin/users.hbs

@ -6,6 +6,9 @@
<thead> <thead>
<tr> <tr>
<th class="vw-account-details">User</th> <th class="vw-account-details">User</th>
{{#if sso_enabled}}
<th class="vw-sso-identifier">SSO Identifier</th>
{{/if}}
<th class="vw-created-at">Created at</th> <th class="vw-created-at">Created at</th>
<th class="vw-last-active">Last Active</th> <th class="vw-last-active">Last Active</th>
<th class="vw-entries">Entries</th> <th class="vw-entries">Entries</th>
@ -38,6 +41,11 @@
</span> </span>
</div> </div>
</td> </td>
{{#if ../sso_enabled}}
<td>
<span class="d-block">{{sso_identifier}}</span>
</td>
{{/if}}
<td> <td>
<span class="d-block">{{created_at}}</span> <span class="d-block">{{created_at}}</span>
</td> </td>
@ -67,6 +75,9 @@
{{/if}} {{/if}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br> <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button><br>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br> <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button><br>
{{#if ../sso_enabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-sso-user>Delete SSO Association</button><br>
{{/if}}
{{#if user_enabled}} {{#if user_enabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br> <button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button><br>
{{else}} {{else}}

Loading…
Cancel
Save