From b60a4a68c7858f54b1d49678c6314473ab873d21 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Sun, 18 Dec 2022 17:57:35 +0100
Subject: [PATCH 1/3] Fix a panic during Yubikey register/login

The yubico crate uses blocking reqwest, and we called the `verify` from
a async thread. To prevent issues we need to wrap it within a
`spawn_blocking`.
---
 src/api/core/two_factor/yubikey.rs | 14 ++++++++------
 src/api/identity.rs                |  2 +-
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs
index 00ef7df2..b076dd00 100644
--- a/src/api/core/two_factor/yubikey.rs
+++ b/src/api/core/two_factor/yubikey.rs
@@ -67,14 +67,16 @@ fn get_yubico_credentials() -> Result<(String, String), Error> {
     }
 }
 
-fn verify_yubikey_otp(otp: String) -> EmptyResult {
+async fn verify_yubikey_otp(otp: String) -> EmptyResult {
     let (yubico_id, yubico_secret) = get_yubico_credentials()?;
 
     let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
 
     match CONFIG.yubico_server() {
-        Some(server) => verify(otp, config.set_api_hosts(vec![server])),
-        None => verify(otp, config),
+        Some(server) => {
+            tokio::task::spawn_blocking(move || verify(otp, config.set_api_hosts(vec![server]))).await.unwrap()
+        }
+        None => tokio::task::spawn_blocking(move || verify(otp, config)).await.unwrap(),
     }
     .map_res("Failed to verify OTP")
     .and(Ok(()))
@@ -152,7 +154,7 @@ async fn activate_yubikey(
             continue;
         }
 
-        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
+        verify_yubikey_otp(yubikey.to_owned()).await.map_res("Invalid Yubikey OTP provided")?;
     }
 
     let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (x[..12]).to_owned()).collect();
@@ -188,7 +190,7 @@ async fn activate_yubikey_put(
     activate_yubikey(data, headers, conn, ip).await
 }
 
-pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
+pub async fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
     if response.len() != 44 {
         err!("Invalid Yubikey OTP length");
     }
@@ -200,7 +202,7 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu
         err!("Given Yubikey is not registered");
     }
 
-    let result = verify_yubikey_otp(response.to_owned());
+    let result = verify_yubikey_otp(response.to_owned()).await;
 
     match result {
         Ok(_answer) => Ok(()),
diff --git a/src/api/identity.rs b/src/api/identity.rs
index 6499ee38..1939aabc 100644
--- a/src/api/identity.rs
+++ b/src/api/identity.rs
@@ -395,7 +395,7 @@ async fn twofactor_auth(
         Some(TwoFactorType::Webauthn) => {
             _tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn).await?
         }
-        Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
+        Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
         Some(TwoFactorType::Duo) => {
             _tf::duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?
         }

From 8e5f03972e978cec9254a7b52bdbb01ffbd891e6 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Thu, 15 Dec 2022 15:57:30 +0100
Subject: [PATCH 2/3] Fix recover-2fa not working.

When audit logging was introduced there entered a small bug preventing
the recover-2fa from working.

This PR fixes that by add a new headers check to extract the device-type
when possible and use that for the logging.

Fixes #2985
---
 src/api/core/two_factor/mod.rs | 11 ++++++++---
 src/api/identity.rs            | 18 +++++++++++-------
 src/auth.rs                    | 22 ++++++++++++++++++++++
 3 files changed, 41 insertions(+), 10 deletions(-)

diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs
index ce3cfb72..a2bbc806 100644
--- a/src/api/core/two_factor/mod.rs
+++ b/src/api/core/two_factor/mod.rs
@@ -6,7 +6,7 @@ use serde_json::Value;
 
 use crate::{
     api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordData},
-    auth::{ClientIp, Headers},
+    auth::{ClientHeaders, ClientIp, Headers},
     crypto,
     db::{models::*, DbConn, DbPool},
     mail, CONFIG,
@@ -73,7 +73,12 @@ struct RecoverTwoFactor {
 }
 
 #[post("/two-factor/recover", data = "<data>")]
-async fn recover(data: JsonUpcase<RecoverTwoFactor>, headers: Headers, mut conn: DbConn, ip: ClientIp) -> JsonResult {
+async fn recover(
+    data: JsonUpcase<RecoverTwoFactor>,
+    client_headers: ClientHeaders,
+    mut conn: DbConn,
+    ip: ClientIp,
+) -> JsonResult {
     let data: RecoverTwoFactor = data.into_inner().data;
 
     use crate::db::models::User;
@@ -97,7 +102,7 @@ async fn recover(data: JsonUpcase<RecoverTwoFactor>, headers: Headers, mut conn:
     // Remove all twofactors from the user
     TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
 
-    log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
+    log_user_event(EventType::UserRecovered2fa as i32, &user.uuid, client_headers.device_type, &ip.ip, &mut conn).await;
 
     // Remove the recovery code, not needed without twofactors
     user.totp_recover = None;
diff --git a/src/api/identity.rs b/src/api/identity.rs
index 1939aabc..0cb1c03a 100644
--- a/src/api/identity.rs
+++ b/src/api/identity.rs
@@ -14,7 +14,7 @@ use crate::{
         core::two_factor::{duo, email, email::EmailTokenData, yubikey},
         ApiResult, EmptyResult, JsonResult, JsonUpcase,
     },
-    auth::ClientIp,
+    auth::{ClientHeaders, ClientIp},
     db::{models::*, DbConn},
     error::MapResult,
     mail, util, CONFIG,
@@ -25,11 +25,10 @@ pub fn routes() -> Vec<Route> {
 }
 
 #[post("/connect/token", data = "<data>")]
-async fn login(data: Form<ConnectData>, mut conn: DbConn, ip: ClientIp) -> JsonResult {
+async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn, ip: ClientIp) -> JsonResult {
     let data: ConnectData = data.into_inner();
 
     let mut user_uuid: Option<String> = None;
-    let device_type = data.device_type.clone();
 
     let login_result = match data.grant_type.as_ref() {
         "refresh_token" => {
@@ -59,15 +58,20 @@ async fn login(data: Form<ConnectData>, mut conn: DbConn, ip: ClientIp) -> JsonR
     };
 
     if let Some(user_uuid) = user_uuid {
-        // When unknown or unable to parse, return 14, which is 'Unknown Browser'
-        let device_type = util::try_parse_string(device_type).unwrap_or(14);
         match &login_result {
             Ok(_) => {
-                log_user_event(EventType::UserLoggedIn as i32, &user_uuid, device_type, &ip.ip, &mut conn).await;
+                log_user_event(
+                    EventType::UserLoggedIn as i32,
+                    &user_uuid,
+                    client_header.device_type,
+                    &ip.ip,
+                    &mut conn,
+                )
+                .await;
             }
             Err(e) => {
                 if let Some(ev) = e.get_event() {
-                    log_user_event(ev.event as i32, &user_uuid, device_type, &ip.ip, &mut conn).await
+                    log_user_event(ev.event as i32, &user_uuid, client_header.device_type, &ip.ip, &mut conn).await
                 }
             }
         }
diff --git a/src/auth.rs b/src/auth.rs
index f3e3af8b..69c5203d 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -315,6 +315,28 @@ impl<'r> FromRequest<'r> for Host {
     }
 }
 
+pub struct ClientHeaders {
+    pub host: String,
+    pub device_type: i32,
+}
+
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for ClientHeaders {
+    type Error = &'static str;
+
+    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
+        let host = try_outcome!(Host::from_request(request).await).host;
+        // When unknown or unable to parse, return 14, which is 'Unknown Browser'
+        let device_type: i32 =
+            request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14);
+
+        Outcome::Success(ClientHeaders {
+            host,
+            device_type,
+        })
+    }
+}
+
 pub struct Headers {
     pub host: String,
     pub device: Device,

From 67a584c1d461016907efdf9bfee4ce26cba29178 Mon Sep 17 00:00:00 2001
From: BlackDex <black.dex@gmail.com>
Date: Thu, 15 Dec 2022 17:15:48 +0100
Subject: [PATCH 3/3] Disable groups by default and Some optimizations

- Put groups support behind a feature flag, and disabled by default.
  The reason is that it has some known issues, but we want to keep
  optimizing this feature. Putting it behind a feature flag could help
  some users, and the developers into optimizing this feature without to
  much trouble.

Further:

- Updates Rust to v1.66.0
- Updated GHA workflows
- Updated Alpine to 3.17
- Updated jquery to v3.6.2
- Moved jdenticon.js to load at the bottom, fixes an issue on chromium
- Added autocomplete attribute to admin login password field
- Added some extra CSP options (Tested this on Safari, Firefox, Chrome, Bitwarden Desktop)
- Moved uppercase convertion from runtime to compile-time using `paste`
  for building the environment variables, lowers heap allocations.
---
 .env.template                                 |   7 ++
 .github/workflows/build.yml                   |   5 +-
 .github/workflows/hadolint.yml                |   5 +-
 .github/workflows/release.yml                 |   5 +-
 .pre-commit-config.yaml                       |   4 +-
 Cargo.lock                                    |  41 +++----
 Cargo.toml                                    |   4 +-
 docker/Dockerfile.j2                          |  18 +--
 docker/amd64/Dockerfile                       |   2 +-
 docker/amd64/Dockerfile.alpine                |   4 +-
 docker/amd64/Dockerfile.buildx                |   2 +-
 docker/amd64/Dockerfile.buildx.alpine         |   4 +-
 docker/arm64/Dockerfile                       |   2 +-
 docker/arm64/Dockerfile.alpine                |   4 +-
 docker/arm64/Dockerfile.buildx                |   2 +-
 docker/arm64/Dockerfile.buildx.alpine         |   4 +-
 docker/armv6/Dockerfile                       |   2 +-
 docker/armv6/Dockerfile.alpine                |   4 +-
 docker/armv6/Dockerfile.buildx                |   2 +-
 docker/armv6/Dockerfile.buildx.alpine         |   4 +-
 docker/armv7/Dockerfile                       |   2 +-
 docker/armv7/Dockerfile.alpine                |   4 +-
 docker/armv7/Dockerfile.buildx                |   2 +-
 docker/armv7/Dockerfile.buildx.alpine         |   4 +-
 rust-toolchain                                |   2 +-
 src/api/core/organizations.rs                 |  44 +++++++
 src/api/web.rs                                |   4 +-
 src/config.rs                                 |   9 +-
 src/db/models/organization.rs                 |   4 +-
 ...ery-3.6.1.slim.js => jquery-3.6.2.slim.js} | 107 ++++++++++++++++--
 src/static/templates/admin/base.hbs           |   2 +-
 src/static/templates/admin/login.hbs          |   2 +-
 src/static/templates/admin/organizations.hbs  |   2 +-
 src/static/templates/admin/users.hbs          |   2 +-
 src/util.rs                                   |   8 +-
 35 files changed, 229 insertions(+), 94 deletions(-)
 rename src/static/scripts/{jquery-3.6.1.slim.js => jquery-3.6.2.slim.js} (98%)

diff --git a/.env.template b/.env.template
index 7ccdacd3..4b323706 100644
--- a/.env.template
+++ b/.env.template
@@ -90,6 +90,13 @@
 ## If unset (the default), events are kept indefinitely and the scheduled job is disabled!
 # EVENTS_DAYS_RETAIN=
 
+## BETA FEATURE: Groups
+## Controls whether group support is enabled for organizations
+## This setting applies to organizations.
+## Disabled by default because this is a beta feature, it contains known issues!
+## KNOW WHAT YOU ARE DOING!
+# ORG_GROUPS_ENABLED=false
+
 ## Job scheduler settings
 ##
 ## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a1afb9c3..b8718a20 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,6 +21,7 @@ on:
 jobs:
   build:
     runs-on: ubuntu-20.04
+    timeout-minutes: 120
     # Make warnings errors, this is to prevent warnings slipping through.
     # This is done globally to prevent rebuilds when the RUSTFLAGS env variable changes.
     env:
@@ -40,7 +41,7 @@ jobs:
     steps:
       # Checkout the repo
       - name: "Checkout"
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
       # End Checkout the repo
 
       # Install dependencies
@@ -78,7 +79,7 @@ jobs:
 
 
       # Enable Rust Caching
-      - uses: Swatinem/rust-cache@b5ec9edd911d3bf82c74038b0a28791e0aa24d6f # v2.0.2
+      - uses: Swatinem/rust-cache@359a70e43a0bb8a13953b04a90f76428b4959bb6 # v2.2.0
       # End Enable Rust Caching
 
 
diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml
index 5b1e77ad..6598311a 100644
--- a/.github/workflows/hadolint.yml
+++ b/.github/workflows/hadolint.yml
@@ -9,10 +9,11 @@ jobs:
   hadolint:
     name: Validate Dockerfile syntax
     runs-on: ubuntu-20.04
+    timeout-minutes: 30
     steps:
       # Checkout the repo
       - name: Checkout
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
       # End Checkout the repo
 
 
@@ -23,7 +24,7 @@ jobs:
           sudo curl -L https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint && \
           sudo chmod +x /usr/local/bin/hadolint
         env:
-          HADOLINT_VERSION: 2.10.0
+          HADOLINT_VERSION: 2.12.0
       # End Download hadolint
 
       # Test Dockerfiles
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index aca21ca5..b3690ceb 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -31,7 +31,7 @@ jobs:
     steps:
       - name: Skip Duplicates Actions
         id: skip_check
-        uses: fkirc/skip-duplicate-actions@f11521568414503656a5af807dc3018c012552c4 # v5.2.0
+        uses: fkirc/skip-duplicate-actions@12aca0a884f6137d619d6a8a09fcc3406ced5281 # v5.3.0
         with:
           cancel_others: 'true'
         # Only run this when not creating a tag
@@ -39,6 +39,7 @@ jobs:
 
   docker-build:
     runs-on: ubuntu-20.04
+    timeout-minutes: 120
     needs: skip_check
     # Start a local docker registry to be used to generate multi-arch images.
     services:
@@ -60,7 +61,7 @@ jobs:
     steps:
       # Checkout the repo
       - name: Checkout
-        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0
+        uses: actions/checkout@755da8c3cf115ac066823e79a1e1788f8940201b # v3.2.0
         with:
           fetch-depth: 0
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index eb44b47f..a8a68242 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,7 +27,7 @@ repos:
       language: system
       args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--"]
       types_or: [rust, file]
-      files: (Cargo.toml|Cargo.lock|.*\.rs$)
+      files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
       pass_filenames: false
     - id: cargo-clippy
       name: cargo clippy
@@ -36,5 +36,5 @@ repos:
       language: system
       args: ["--features", "sqlite,mysql,postgresql,enable_mimalloc", "--", "-D", "warnings"]
       types_or: [rust, file]
-      files: (Cargo.toml|Cargo.lock|.*\.rs$)
+      files: (Cargo.toml|Cargo.lock|rust-toolchain|.*\.rs$)
       pass_filenames: false
diff --git a/Cargo.lock b/Cargo.lock
index 5da48e65..8bf1ee42 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
 
 [[package]]
 name = "addr2line"
-version = "0.17.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
 dependencies = [
  "gimli",
 ]
@@ -165,15 +165,15 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "backtrace"
-version = "0.3.66"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
 dependencies = [
  "addr2line",
  "cc",
  "cfg-if",
  "libc",
- "miniz_oxide 0.5.4",
+ "miniz_oxide",
  "object",
  "rustc-demangle",
 ]
@@ -283,9 +283,9 @@ checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663"
 
 [[package]]
 name = "cc"
-version = "1.0.77"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
 
 [[package]]
 name = "cfg-if"
@@ -773,7 +773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.6.2",
+ "miniz_oxide",
 ]
 
 [[package]]
@@ -947,9 +947,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.26.2"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793"
 
 [[package]]
 name = "glob"
@@ -1492,15 +1492,6 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
-[[package]]
-name = "miniz_oxide"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
-dependencies = [
- "adler",
-]
-
 [[package]]
 name = "miniz_oxide"
 version = "0.6.2"
@@ -1663,9 +1654,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.29.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb"
 dependencies = [
  "memchr",
 ]
@@ -1777,9 +1768,9 @@ dependencies = [
 
 [[package]]
 name = "paste"
-version = "1.0.9"
+version = "1.0.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+checksum = "cf1c2c742266c2f1041c914ba65355a83ae8747b05f208319784083583494b4b"
 
 [[package]]
 name = "pear"
@@ -2891,9 +2882,9 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.5.9"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
 dependencies = [
  "serde",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 3dcbe689..4f4e4f0f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -44,7 +44,7 @@ log = "0.4.17"
 fern = { version = "0.6.1", features = ["syslog-6"] }
 tracing = { version = "0.1.37", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
 
-backtrace = "0.3.66" # Logging panics to logfile instead stderr only
+backtrace = "0.3.67" # Logging panics to logfile instead stderr only
 
 # A `dotenv` implementation for Rust
 dotenvy = { version = "0.15.6", default-features = false }
@@ -142,7 +142,7 @@ openssl = "0.10.44"
 pico-args = "0.5.0"
 
 # Macro ident concatenation
-paste = "1.0.9"
+paste = "1.0.10"
 governor = "0.5.1"
 
 # Check client versions for specific features.
diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2
index 826f1360..54f3f325 100644
--- a/docker/Dockerfile.j2
+++ b/docker/Dockerfile.j2
@@ -3,23 +3,23 @@
 # This file was generated using a Jinja2 template.
 # Please make your changes in `Dockerfile.j2` and then `make` the individual Dockerfiles.
 
-{% set build_stage_base_image = "rust:1.65-bullseye" %}
+{% set build_stage_base_image = "rust:1.66-bullseye" %}
 {% if "alpine" in target_file %}
 {%   if "amd64" in target_file %}
-{%     set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-stable-1.65.0" %}
-{%     set runtime_stage_base_image = "alpine:3.16" %}
+{%     set build_stage_base_image = "blackdex/rust-musl:x86_64-musl-stable-1.66.0" %}
+{%     set runtime_stage_base_image = "alpine:3.17" %}
 {%     set package_arch_target = "x86_64-unknown-linux-musl" %}
 {%   elif "armv7" in target_file %}
-{%     set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-stable-1.65.0" %}
-{%     set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.16" %}
+{%     set build_stage_base_image = "blackdex/rust-musl:armv7-musleabihf-stable-1.66.0" %}
+{%     set runtime_stage_base_image = "balenalib/armv7hf-alpine:3.17" %}
 {%     set package_arch_target = "armv7-unknown-linux-musleabihf" %}
 {%   elif "armv6" in target_file %}
-{%     set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-stable-1.65.0" %}
-{%     set runtime_stage_base_image = "balenalib/rpi-alpine:3.16" %}
+{%     set build_stage_base_image = "blackdex/rust-musl:arm-musleabi-stable-1.66.0" %}
+{%     set runtime_stage_base_image = "balenalib/rpi-alpine:3.17" %}
 {%     set package_arch_target = "arm-unknown-linux-musleabi" %}
 {%   elif "arm64" in target_file %}
-{%     set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-stable-1.65.0" %}
-{%     set runtime_stage_base_image = "balenalib/aarch64-alpine:3.16" %}
+{%     set build_stage_base_image = "blackdex/rust-musl:aarch64-musl-stable-1.66.0" %}
+{%     set runtime_stage_base_image = "balenalib/aarch64-alpine:3.17" %}
 {%     set package_arch_target = "aarch64-unknown-linux-musl" %}
 {%   endif %}
 {% elif "amd64" in target_file %}
diff --git a/docker/amd64/Dockerfile b/docker/amd64/Dockerfile
index a93e5b39..32af309f 100644
--- a/docker/amd64/Dockerfile
+++ b/docker/amd64/Dockerfile
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/amd64/Dockerfile.alpine b/docker/amd64/Dockerfile.alpine
index 26faf679..4f05a095 100644
--- a/docker/amd64/Dockerfile.alpine
+++ b/docker/amd64/Dockerfile.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:x86_64-musl-stable-1.65.0 as build
+FROM blackdex/rust-musl:x86_64-musl-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN cargo build --features ${DB} --release --target=x86_64-unknown-linux-musl
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM alpine:3.16
+FROM alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/amd64/Dockerfile.buildx b/docker/amd64/Dockerfile.buildx
index ae901769..c93d1a2d 100644
--- a/docker/amd64/Dockerfile.buildx
+++ b/docker/amd64/Dockerfile.buildx
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/amd64/Dockerfile.buildx.alpine b/docker/amd64/Dockerfile.buildx.alpine
index 8bb49c28..bd018afd 100644
--- a/docker/amd64/Dockerfile.buildx.alpine
+++ b/docker/amd64/Dockerfile.buildx.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:x86_64-musl-stable-1.65.0 as build
+FROM blackdex/rust-musl:x86_64-musl-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM alpine:3.16
+FROM alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/arm64/Dockerfile b/docker/arm64/Dockerfile
index e45f0d8c..62f4549d 100644
--- a/docker/arm64/Dockerfile
+++ b/docker/arm64/Dockerfile
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/arm64/Dockerfile.alpine b/docker/arm64/Dockerfile.alpine
index ab0b3dd5..77f8a013 100644
--- a/docker/arm64/Dockerfile.alpine
+++ b/docker/arm64/Dockerfile.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:aarch64-musl-stable-1.65.0 as build
+FROM blackdex/rust-musl:aarch64-musl-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-musl
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/aarch64-alpine:3.16
+FROM balenalib/aarch64-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/arm64/Dockerfile.buildx b/docker/arm64/Dockerfile.buildx
index 0ac3403e..c8c4356d 100644
--- a/docker/arm64/Dockerfile.buildx
+++ b/docker/arm64/Dockerfile.buildx
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/arm64/Dockerfile.buildx.alpine b/docker/arm64/Dockerfile.buildx.alpine
index c56642a9..d293c26c 100644
--- a/docker/arm64/Dockerfile.buildx.alpine
+++ b/docker/arm64/Dockerfile.buildx.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:aarch64-musl-stable-1.65.0 as build
+FROM blackdex/rust-musl:aarch64-musl-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/aarch64-alpine:3.16
+FROM balenalib/aarch64-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/armv6/Dockerfile b/docker/armv6/Dockerfile
index 6d0ef129..7f4ec834 100644
--- a/docker/armv6/Dockerfile
+++ b/docker/armv6/Dockerfile
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/armv6/Dockerfile.alpine b/docker/armv6/Dockerfile.alpine
index 3d0f390f..879d1061 100644
--- a/docker/armv6/Dockerfile.alpine
+++ b/docker/armv6/Dockerfile.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:arm-musleabi-stable-1.65.0 as build
+FROM blackdex/rust-musl:arm-musleabi-stable-1.66.0 as build
 
 
 
@@ -83,7 +83,7 @@ RUN cargo build --features ${DB} --release --target=arm-unknown-linux-musleabi
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/rpi-alpine:3.16
+FROM balenalib/rpi-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/armv6/Dockerfile.buildx b/docker/armv6/Dockerfile.buildx
index 338e3193..7e543ef5 100644
--- a/docker/armv6/Dockerfile.buildx
+++ b/docker/armv6/Dockerfile.buildx
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/armv6/Dockerfile.buildx.alpine b/docker/armv6/Dockerfile.buildx.alpine
index 248b8c16..4320cb76 100644
--- a/docker/armv6/Dockerfile.buildx.alpine
+++ b/docker/armv6/Dockerfile.buildx.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:arm-musleabi-stable-1.65.0 as build
+FROM blackdex/rust-musl:arm-musleabi-stable-1.66.0 as build
 
 
 
@@ -83,7 +83,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/rpi-alpine:3.16
+FROM balenalib/rpi-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/armv7/Dockerfile b/docker/armv7/Dockerfile
index 49d91ad0..d279cc65 100644
--- a/docker/armv7/Dockerfile
+++ b/docker/armv7/Dockerfile
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/armv7/Dockerfile.alpine b/docker/armv7/Dockerfile.alpine
index a818461d..691900e2 100644
--- a/docker/armv7/Dockerfile.alpine
+++ b/docker/armv7/Dockerfile.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:armv7-musleabihf-stable-1.65.0 as build
+FROM blackdex/rust-musl:armv7-musleabihf-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-musleabi
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/armv7hf-alpine:3.16
+FROM balenalib/armv7hf-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/docker/armv7/Dockerfile.buildx b/docker/armv7/Dockerfile.buildx
index 1630d49a..de9887ad 100644
--- a/docker/armv7/Dockerfile.buildx
+++ b/docker/armv7/Dockerfile.buildx
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM rust:1.65-bullseye as build
+FROM rust:1.66-bullseye as build
 
 
 
diff --git a/docker/armv7/Dockerfile.buildx.alpine b/docker/armv7/Dockerfile.buildx.alpine
index 9c7fe045..49d66f52 100644
--- a/docker/armv7/Dockerfile.buildx.alpine
+++ b/docker/armv7/Dockerfile.buildx.alpine
@@ -27,7 +27,7 @@
 FROM vaultwarden/web-vault@sha256:1f124e2d1a8e9678d7b9d17587b6340fba0db298e96995552698108a43f5c1c1 as vault
 
 ########################## BUILD IMAGE  ##########################
-FROM blackdex/rust-musl:armv7-musleabihf-stable-1.65.0 as build
+FROM blackdex/rust-musl:armv7-musleabihf-stable-1.66.0 as build
 
 
 
@@ -81,7 +81,7 @@ RUN --mount=type=cache,target=/root/.cargo/git --mount=type=cache,target=/root/.
 ######################## RUNTIME IMAGE  ########################
 # Create a new stage with a minimal image
 # because we already have a binary built
-FROM balenalib/armv7hf-alpine:3.16
+FROM balenalib/armv7hf-alpine:3.17
 
 ENV ROCKET_PROFILE="release" \
     ROCKET_ADDRESS=0.0.0.0 \
diff --git a/rust-toolchain b/rust-toolchain
index 902c7418..b6148bc0 100644
--- a/rust-toolchain
+++ b/rust-toolchain
@@ -1 +1 @@
-1.65.0
+1.66.0
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index e605080d..d50a1f12 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -1990,6 +1990,10 @@ async fn _restore_organization_user(
 
 #[get("/organizations/<org_id>/groups")]
 async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let groups = Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>();
 
     Ok(Json(json!({
@@ -2089,6 +2093,10 @@ async fn post_groups(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let group_request = data.into_inner().data;
     let group = group_request.to_group(&org_id)?;
 
@@ -2115,6 +2123,10 @@ async fn put_group(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
         None => err!("Group not found"),
@@ -2159,6 +2171,10 @@ async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>,
 
 #[get("/organizations/<_org_id>/groups/<group_id>/details")]
 async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
         _ => err!("Group could not be found!"),
@@ -2199,6 +2215,10 @@ async fn delete_group(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> EmptyResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
         _ => err!("Group not found"),
@@ -2220,6 +2240,10 @@ async fn delete_group(
 
 #[get("/organizations/<_org_id>/groups/<group_id>")]
 async fn get_group(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     let group = match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(group) => group,
         _ => err!("Group not found"),
@@ -2230,6 +2254,10 @@ async fn get_group(_org_id: String, group_id: String, _headers: AdminHeaders, mu
 
 #[get("/organizations/<_org_id>/groups/<group_id>/users")]
 async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("Group could not be found!"),
@@ -2253,6 +2281,10 @@ async fn put_group_users(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> EmptyResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     match Group::find_by_uuid(&group_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("Group could not be found!"),
@@ -2282,6 +2314,10 @@ async fn put_group_users(
 
 #[get("/organizations/<_org_id>/users/<user_id>/groups")]
 async fn get_user_groups(_org_id: String, user_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     match UserOrganization::find_by_uuid(&user_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("User could not be found!"),
@@ -2320,6 +2356,10 @@ async fn put_user_groups(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> EmptyResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("User could not be found!"),
@@ -2368,6 +2408,10 @@ async fn delete_group_user(
     mut conn: DbConn,
     ip: ClientIp,
 ) -> EmptyResult {
+    if !CONFIG.org_groups_enabled() {
+        err!("Group support is disabled");
+    }
+
     match UserOrganization::find_by_uuid(&org_user_id, &mut conn).await {
         Some(_) => { /* Do nothing */ }
         _ => err!("User could not be found!"),
diff --git a/src/api/web.rs b/src/api/web.rs
index 72bb66d0..3742a088 100644
--- a/src/api/web.rs
+++ b/src/api/web.rs
@@ -107,8 +107,8 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
         "jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
         "datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
         "datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
-        "jquery-3.6.1.slim.js" => {
-            Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.1.slim.js")))
+        "jquery-3.6.2.slim.js" => {
+            Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.2.slim.js")))
         }
         _ => err!(format!("Static file not found: {}", filename)),
     }
diff --git a/src/config.rs b/src/config.rs
index edf5bbfe..00d4737c 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -85,7 +85,7 @@ macro_rules! make_config {
 
                 let mut builder = ConfigBuilder::default();
                 $($(
-                    builder.$name = make_config! { @getenv &stringify!($name).to_uppercase(), $ty };
+                    builder.$name = make_config! { @getenv paste::paste!(stringify!([<$name:upper>])), $ty };
                 )+)+
 
                 builder
@@ -105,7 +105,7 @@ macro_rules! make_config {
                         builder.$name = v.clone();
 
                         if self.$name.is_some() {
-                            overrides.push(stringify!($name).to_uppercase());
+                            overrides.push(paste::paste!(stringify!([<$name:upper>])).into());
                         }
                     }
                 )+)+
@@ -195,7 +195,7 @@ macro_rules! make_config {
                                 element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
                                 element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
                                 element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
-                                element.insert("overridden".into(), (overriden.contains(&stringify!($name).to_uppercase())).into());
+                                element.insert("overridden".into(), (overriden.contains(&paste::paste!(stringify!([<$name:upper>])).into())).into());
                                 element
                             }),
                         )+
@@ -564,6 +564,9 @@ make_config! {
         admin_ratelimit_seconds:       u64, false, def, 300;
         /// Max burst size for admin login requests |> Allow a burst of requests of up to this size, while maintaining the average indicated by `admin_ratelimit_seconds`
         admin_ratelimit_max_burst:     u32, false, def, 3;
+
+        /// Enable groups (BETA!) (Know the risks!) |> Enables groups support for organizations (Currently contains known issues!).
+        org_groups_enabled:     bool,   false,  def,    false;
     },
 
     /// Yubikey settings
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 3bc2ddad..a2ab75a7 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -149,7 +149,7 @@ impl Organization {
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
             "UseEvents": CONFIG.org_events_enabled(),
-            "UseGroups": true,
+            "UseGroups": CONFIG.org_groups_enabled(),
             "UseTotp": true,
             "UsePolicies": true,
             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
@@ -304,7 +304,7 @@ impl UserOrganization {
             "Use2fa": true,
             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
             "UseEvents": CONFIG.org_events_enabled(),
-            "UseGroups": true,
+            "UseGroups": CONFIG.org_groups_enabled(),
             "UseTotp": true,
             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
             "UsePolicies": true,
diff --git a/src/static/scripts/jquery-3.6.1.slim.js b/src/static/scripts/jquery-3.6.2.slim.js
similarity index 98%
rename from src/static/scripts/jquery-3.6.1.slim.js
rename to src/static/scripts/jquery-3.6.2.slim.js
index 91512c95..4c41f3eb 100644
--- a/src/static/scripts/jquery-3.6.1.slim.js
+++ b/src/static/scripts/jquery-3.6.2.slim.js
@@ -1,5 +1,5 @@
 /*!
- * jQuery JavaScript Library v3.6.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
+ * jQuery JavaScript Library v3.6.2 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
  * https://jquery.com/
  *
  * Includes Sizzle.js
@@ -9,7 +9,7 @@
  * Released under the MIT license
  * https://jquery.org/license
  *
- * Date: 2022-08-26T17:52Z
+ * Date: 2022-12-13T14:56Z
  */
 ( function( global, factory ) {
 
@@ -151,7 +151,7 @@ function toType( obj ) {
 
 
 var
-	version = "3.6.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
+	version = "3.6.2 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
 
 	// Define a local copy of jQuery
 	jQuery = function( selector, context ) {
@@ -522,14 +522,14 @@ function isArrayLike( obj ) {
 }
 var Sizzle =
 /*!
- * Sizzle CSS Selector Engine v2.3.6
+ * Sizzle CSS Selector Engine v2.3.8
  * https://sizzlejs.com/
  *
  * Copyright JS Foundation and other contributors
  * Released under the MIT license
  * https://js.foundation/
  *
- * Date: 2021-02-16
+ * Date: 2022-11-16
  */
 ( function( window ) {
 var i,
@@ -879,6 +879,27 @@ function Sizzle( selector, context, results, seed ) {
 				}
 
 				try {
+
+					// `qSA` may not throw for unrecognized parts using forgiving parsing:
+					// https://drafts.csswg.org/selectors/#forgiving-selector
+					// like the `:has()` pseudo-class:
+					// https://drafts.csswg.org/selectors/#relational
+					// `CSS.supports` is still expected to return `false` then:
+					// https://drafts.csswg.org/css-conditional-4/#typedef-supports-selector-fn
+					// https://drafts.csswg.org/css-conditional-4/#dfn-support-selector
+					if ( support.cssSupportsSelector &&
+
+						// eslint-disable-next-line no-undef
+						!CSS.supports( "selector(" + newSelector + ")" ) ) {
+
+						// Support: IE 11+
+						// Throw to get to the same code path as an error directly in qSA.
+						// Note: once we only support browser supporting
+						// `CSS.supports('selector(...)')`, we can most likely drop
+						// the `try-catch`. IE doesn't implement the API.
+						throw new Error();
+					}
+
 					push.apply( results,
 						newContext.querySelectorAll( newSelector )
 					);
@@ -1174,6 +1195,31 @@ setDocument = Sizzle.setDocument = function( node ) {
 			!el.querySelectorAll( ":scope fieldset div" ).length;
 	} );
 
+	// Support: Chrome 105+, Firefox 104+, Safari 15.4+
+	// Make sure forgiving mode is not used in `CSS.supports( "selector(...)" )`.
+	//
+	// `:is()` uses a forgiving selector list as an argument and is widely
+	// implemented, so it's a good one to test against.
+	support.cssSupportsSelector = assert( function() {
+		/* eslint-disable no-undef */
+
+		return CSS.supports( "selector(*)" ) &&
+
+			// Support: Firefox 78-81 only
+			// In old Firefox, `:is()` didn't use forgiving parsing. In that case,
+			// fail this test as there's no selector to test against that.
+			// `CSS.supports` uses unforgiving parsing
+			document.querySelectorAll( ":is(:jqfake)" ) &&
+
+			// `*` is needed as Safari & newer Chrome implemented something in between
+			// for `:has()` - it throws in `qSA` if it only contains an unsupported
+			// argument but multiple ones, one of which is supported, are fine.
+			// We want to play safe in case `:is()` gets the same treatment.
+			!CSS.supports( "selector(:is(*,:jqfake))" );
+
+		/* eslint-enable */
+	} );
+
 	/* Attributes
 	---------------------------------------------------------------------- */
 
@@ -1440,6 +1486,18 @@ setDocument = Sizzle.setDocument = function( node ) {
 		} );
 	}
 
+	if ( !support.cssSupportsSelector ) {
+
+		// Support: Chrome 105+, Safari 15.4+
+		// `:has()` uses a forgiving selector list as an argument so our regular
+		// `try-catch` mechanism fails to catch `:has()` with arguments not supported
+		// natively like `:has(:contains("Foo"))`. Where supported & spec-compliant,
+		// we now use `CSS.supports("selector(SELECTOR_TO_BE_TESTED)")` but outside
+		// that, let's mark `:has` as buggy to always use jQuery traversal for
+		// `:has()`.
+		rbuggyQSA.push( ":has" );
+	}
+
 	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );
 	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) );
 
@@ -1452,7 +1510,14 @@ setDocument = Sizzle.setDocument = function( node ) {
 	// As in, an element does not contain itself
 	contains = hasCompare || rnative.test( docElem.contains ) ?
 		function( a, b ) {
-			var adown = a.nodeType === 9 ? a.documentElement : a,
+
+			// Support: IE <9 only
+			// IE doesn't have `contains` on `document` so we need to check for
+			// `documentElement` presence.
+			// We need to fall back to `a` when `documentElement` is missing
+			// as `ownerDocument` of elements within `<template/>` may have
+			// a null one - a default behavior of all modern browsers.
+			var adown = a.nodeType === 9 && a.documentElement || a,
 				bup = b && b.parentNode;
 			return a === bup || !!( bup && bup.nodeType === 1 && (
 				adown.contains ?
@@ -2242,7 +2307,7 @@ Expr = Sizzle.selectors = {
 			return elem.nodeName.toLowerCase() === "input" &&
 				elem.type === "text" &&
 
-				// Support: IE<8
+				// Support: IE <10 only
 				// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
 				( ( attr = elem.getAttribute( "type" ) ) == null ||
 					attr.toLowerCase() === "text" );
@@ -6608,17 +6673,37 @@ function curCSS( elem, name, computed ) {
 	//   .css('filter') (IE 9 only, trac-12537)
 	//   .css('--customProperty) (gh-3144)
 	if ( computed ) {
+
+		// Support: IE <=9 - 11+
+		// IE only supports `"float"` in `getPropertyValue`; in computed styles
+		// it's only available as `"cssFloat"`. We no longer modify properties
+		// sent to `.css()` apart from camelCasing, so we need to check both.
+		// Normally, this would create difference in behavior: if
+		// `getPropertyValue` returns an empty string, the value returned
+		// by `.css()` would be `undefined`. This is usually the case for
+		// disconnected elements. However, in IE even disconnected elements
+		// with no styles return `"none"` for `getPropertyValue( "float" )`
 		ret = computed.getPropertyValue( name ) || computed[ name ];
 
-		// trim whitespace for custom property (issue gh-4926)
-		if ( isCustomProp ) {
+		if ( isCustomProp && ret ) {
 
-			// rtrim treats U+000D CARRIAGE RETURN and U+000C FORM FEED
+			// Support: Firefox 105+, Chrome <=105+
+			// Spec requires trimming whitespace for custom properties (gh-4926).
+			// Firefox only trims leading whitespace. Chrome just collapses
+			// both leading & trailing whitespace to a single space.
+			//
+			// Fall back to `undefined` if empty string returned.
+			// This collapses a missing definition with property defined
+			// and set to an empty string but there's no standard API
+			// allowing us to differentiate them without a performance penalty
+			// and returning `undefined` aligns with older jQuery.
+			//
+			// rtrimCSS treats U+000D CARRIAGE RETURN and U+000C FORM FEED
 			// as whitespace while CSS does not, but this is not a problem
 			// because CSS preprocessing replaces them with U+000A LINE FEED
 			// (which *is* CSS whitespace)
 			// https://www.w3.org/TR/css-syntax-3/#input-preprocessing
-			ret = ret.replace( rtrimCSS, "$1" );
+			ret = ret.replace( rtrimCSS, "$1" ) || undefined;
 		}
 
 		if ( ret === "" && !isAttached( elem ) ) {
diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs
index b9ccbfae..ba3e88d8 100644
--- a/src/static/templates/admin/base.hbs
+++ b/src/static/templates/admin/base.hbs
@@ -28,7 +28,6 @@
             border: var(--bs-alert-border);
         }
     </style>
-    <script src="{{urlpath}}/vw_static/jdenticon.js"></script>
     <script>
         'use strict';
 
@@ -141,6 +140,7 @@
             }
         })();
     </script>
+    <script src="{{urlpath}}/vw_static/jdenticon.js"></script>
     <script src="{{urlpath}}/vw_static/bootstrap-native.js"></script>
 </body>
 </html>
diff --git a/src/static/templates/admin/login.hbs b/src/static/templates/admin/login.hbs
index 20b1998e..61d4daf9 100644
--- a/src/static/templates/admin/login.hbs
+++ b/src/static/templates/admin/login.hbs
@@ -13,7 +13,7 @@
             <small>Please provide it below:</small>
 
             <form class="form-inline" method="post" action="{{urlpath}}/admin">
-                <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token" autofocus="autofocus">
+                <input type="password" autocomplete="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token" autofocus="autofocus">
                 {{#if redirect}}
                 <input type="hidden" id="redirect" name="redirect" value="/{{redirect}}">
                 {{/if}}
diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs
index 09873ab7..9762b189 100644
--- a/src/static/templates/admin/organizations.hbs
+++ b/src/static/templates/admin/organizations.hbs
@@ -49,7 +49,7 @@
 </main>
 
 <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
-<script src="{{urlpath}}/vw_static/jquery-3.6.1.slim.js"></script>
+<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
 <script src="{{urlpath}}/vw_static/datatables.js"></script>
 <script>
     'use strict';
diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs
index 64e4188d..5825720f 100644
--- a/src/static/templates/admin/users.hbs
+++ b/src/static/templates/admin/users.hbs
@@ -136,7 +136,7 @@
 </main>
 
 <link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
-<script src="{{urlpath}}/vw_static/jquery-3.6.1.slim.js"></script>
+<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
 <script src="{{urlpath}}/vw_static/datatables.js"></script>
 <script>
     'use strict';
diff --git a/src/util.rs b/src/util.rs
index c3dde2bb..a05baddb 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -63,6 +63,8 @@ impl Fairing for AppHeaders {
             // app.simplelogin.io, app.anonaddy.com, api.fastmail.com, quack.duckduckgo.com
             let csp = format!(
                 "default-src 'self'; \
+                base-uri 'self'; \
+                form-action 'self'; \
                 object-src 'self' blob:; \
                 script-src 'self'{script_src}; \
                 style-src 'self' 'unsafe-inline'; \
@@ -74,12 +76,12 @@ impl Fairing for AppHeaders {
                   moz-extension://* \
                   {allowed_iframe_ancestors}; \
                 img-src 'self' data: \
-                  https://haveibeenpwned.com/ \
+                  https://haveibeenpwned.com \
                   https://www.gravatar.com \
                   {icon_service_csp}; \
                 connect-src 'self' \
-                  https://api.pwnedpasswords.com/range/ \
-                  https://2fa.directory/api/ \
+                  https://api.pwnedpasswords.com \
+                  https://2fa.directory \
                   https://app.simplelogin.io/api/ \
                   https://app.anonaddy.com/api/ \
                   https://api.fastmail.com/ \