mirror of
https://github.com/apache/superset.git
synced 2026-06-16 13:09:20 +00:00
Compare commits
15 Commits
bump-setup
...
fix/smtp-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bc90dc8b9 | ||
|
|
0b40d8d438 | ||
|
|
c76606f48a | ||
|
|
5288083a79 | ||
|
|
7fc50614e8 | ||
|
|
1c994194e3 | ||
|
|
6b466225fb | ||
|
|
379435b7eb | ||
|
|
4b96b91b53 | ||
|
|
073599bd0c | ||
|
|
9e2c4533c8 | ||
|
|
1ae115981f | ||
|
|
b078ae4b51 | ||
|
|
48b755470f | ||
|
|
dad7dae4f6 |
26
UPDATING.md
26
UPDATING.md
@@ -121,6 +121,32 @@ This change is backward compatible. The feature is off by default, and even when
|
||||
|
||||
Disabling a user account (setting `active` to `False`, via the admin UI, REST API, or CLI) now terminates that user's outstanding sessions on their next request, instead of relying on a passive check. This works for both client-side cookie sessions and server-side session stores via a per-user invalidation epoch (`user_attribute.sessions_invalidated_at`, added by a migration). The mechanism is inert for users that were never disabled (NULL epoch), so there is no behavior change for active users. Re-enabling an account and logging in again starts a fresh, valid session. The migration backfills the epoch for accounts that are already disabled at upgrade time, so re-enabling such an account does not revive a session that predates this feature.
|
||||
|
||||
### Opt-in SSH tunnel server host key verification
|
||||
|
||||
SSH tunnels can now optionally pin the expected SSH server host key as a defense-in-depth measure against man-in-the-middle attacks. paramiko's transport performs no known-hosts checking by default, so previously the SSH server's identity was not verified. This feature is opt-in and off by default; existing tunnels are unaffected.
|
||||
|
||||
- A new nullable `server_host_key` column on the `ssh_tunnels` table stores the expected host key in authorized-key form (e.g. `ssh-ed25519 AAAA...`). It is a public key and is stored in plaintext. It can be set via the SSH tunnel POST/PUT payloads (`ssh_tunnel.server_host_key`).
|
||||
- When a tunnel has `server_host_key` set, Superset connects to the SSH server, reads the host key it presents, and rejects the tunnel if it does not match.
|
||||
- A new config flag `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING` (default `False`) controls fail-closed behavior. When `True`, every tunnel must declare a `server_host_key`; a tunnel without one is rejected.
|
||||
|
||||
Runbook to adopt:
|
||||
|
||||
1. Capture the SSH server's host key, e.g. `ssh-keyscan -t ed25519 ssh.example.com` (verify it out-of-band).
|
||||
2. Set that value on the tunnel's `server_host_key` (via the database/SSH tunnel API or UI payload).
|
||||
3. Optionally set `SSH_TUNNEL_STRICT_HOST_KEY_CHECKING = True` in `superset_config.py` to require host-key verification on all tunnels.
|
||||
|
||||
### SMTP server certificate validation enabled by default
|
||||
|
||||
`SMTP_SSL_SERVER_AUTH` now defaults to `True` (previously `False`). With this default, STARTTLS/SSL connections to the configured SMTP server validate the server's TLS certificate against the system trusted CA store. This makes outbound email (alerts and reports) verify the mail server's identity out of the box.
|
||||
|
||||
If your SMTP server presents a self-signed certificate, or a certificate that is not trusted by the system CA store, email delivery may now fail with a certificate verification error. To restore the previous behavior of skipping certificate validation, set the following in `superset_config.py`:
|
||||
|
||||
```python
|
||||
SMTP_SSL_SERVER_AUTH = False
|
||||
```
|
||||
|
||||
The recommended fix is to add the SMTP server's certificate (or its issuing CA) to the system trust store rather than disabling validation.
|
||||
|
||||
### Dataset import validates catalog against the target connection
|
||||
|
||||
Importing a dataset now validates the `catalog` field against the target database connection. When the connection has multi-catalog disabled (`allow_multi_catalog` off) and the dataset's catalog is not the connection's default catalog, the import fails instead of silently persisting the non-default catalog. This matches the validation already enforced on the dataset update path and prevents imported datasets from querying an unintended database.
|
||||
|
||||
158
docs/yarn.lock
158
docs/yarn.lock
@@ -265,6 +265,15 @@
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/code-frame@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7"
|
||||
integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
js-tokens "^4.0.0"
|
||||
picocolors "^1.1.1"
|
||||
|
||||
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
|
||||
version "7.28.0"
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
|
||||
@@ -275,20 +284,25 @@
|
||||
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz"
|
||||
integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==
|
||||
|
||||
"@babel/compat-data@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629"
|
||||
integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==
|
||||
|
||||
"@babel/core@^7.21.3", "@babel/core@^7.25.9":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz"
|
||||
integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7"
|
||||
integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.28.6"
|
||||
"@babel/generator" "^7.28.6"
|
||||
"@babel/helper-compilation-targets" "^7.28.6"
|
||||
"@babel/helper-module-transforms" "^7.28.6"
|
||||
"@babel/helpers" "^7.28.6"
|
||||
"@babel/parser" "^7.28.6"
|
||||
"@babel/template" "^7.28.6"
|
||||
"@babel/traverse" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
"@babel/code-frame" "^7.29.7"
|
||||
"@babel/generator" "^7.29.7"
|
||||
"@babel/helper-compilation-targets" "^7.29.7"
|
||||
"@babel/helper-module-transforms" "^7.29.7"
|
||||
"@babel/helpers" "^7.29.7"
|
||||
"@babel/parser" "^7.29.7"
|
||||
"@babel/template" "^7.29.7"
|
||||
"@babel/traverse" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
"@jridgewell/remapping" "^2.3.5"
|
||||
convert-source-map "^2.0.0"
|
||||
debug "^4.1.0"
|
||||
@@ -318,6 +332,17 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/generator@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3"
|
||||
integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
"@jridgewell/gen-mapping" "^0.3.12"
|
||||
"@jridgewell/trace-mapping" "^0.3.28"
|
||||
jsesc "^3.0.2"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3":
|
||||
version "7.27.3"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
|
||||
@@ -325,7 +350,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.27.3"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6":
|
||||
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz"
|
||||
integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==
|
||||
@@ -336,6 +361,17 @@
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-compilation-targets@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042"
|
||||
integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.29.7"
|
||||
"@babel/helper-validator-option" "^7.29.7"
|
||||
browserslist "^4.24.0"
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz"
|
||||
@@ -374,6 +410,11 @@
|
||||
resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz"
|
||||
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
|
||||
|
||||
"@babel/helper-globals@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b"
|
||||
integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==
|
||||
|
||||
"@babel/helper-member-expression-to-functions@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz"
|
||||
@@ -398,6 +439,14 @@
|
||||
"@babel/traverse" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
|
||||
"@babel/helper-module-imports@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396"
|
||||
integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==
|
||||
dependencies:
|
||||
"@babel/traverse" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
|
||||
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz"
|
||||
@@ -407,6 +456,15 @@
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
"@babel/traverse" "^7.28.6"
|
||||
|
||||
"@babel/helper-module-transforms@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae"
|
||||
integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.29.7"
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
"@babel/traverse" "^7.29.7"
|
||||
|
||||
"@babel/helper-optimise-call-expression@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz"
|
||||
@@ -455,16 +513,31 @@
|
||||
resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
|
||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||
|
||||
"@babel/helper-string-parser@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f"
|
||||
integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz"
|
||||
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2"
|
||||
integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==
|
||||
|
||||
"@babel/helper-validator-option@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz"
|
||||
integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
|
||||
|
||||
"@babel/helper-validator-option@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a"
|
||||
integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==
|
||||
|
||||
"@babel/helper-wrap-function@^7.27.1":
|
||||
version "7.28.3"
|
||||
resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz"
|
||||
@@ -474,13 +547,13 @@
|
||||
"@babel/traverse" "^7.28.3"
|
||||
"@babel/types" "^7.28.2"
|
||||
|
||||
"@babel/helpers@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz"
|
||||
integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==
|
||||
"@babel/helpers@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607"
|
||||
integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==
|
||||
dependencies:
|
||||
"@babel/template" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
"@babel/template" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
|
||||
"@babel/parser@^7.28.6":
|
||||
version "7.28.6"
|
||||
@@ -496,6 +569,13 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.29.0"
|
||||
|
||||
"@babel/parser@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334"
|
||||
integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==
|
||||
dependencies:
|
||||
"@babel/types" "^7.29.7"
|
||||
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz"
|
||||
@@ -1172,6 +1252,15 @@
|
||||
"@babel/parser" "^7.28.6"
|
||||
"@babel/types" "^7.28.6"
|
||||
|
||||
"@babel/template@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
|
||||
integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.29.7"
|
||||
"@babel/parser" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
|
||||
"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.6":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz"
|
||||
@@ -1198,6 +1287,19 @@
|
||||
"@babel/types" "^7.29.0"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/traverse@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d"
|
||||
integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.29.7"
|
||||
"@babel/generator" "^7.29.7"
|
||||
"@babel/helper-globals" "^7.29.7"
|
||||
"@babel/parser" "^7.29.7"
|
||||
"@babel/template" "^7.29.7"
|
||||
"@babel/types" "^7.29.7"
|
||||
debug "^4.3.1"
|
||||
|
||||
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.6", "@babel/types@^7.4.4":
|
||||
version "7.28.6"
|
||||
resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz"
|
||||
@@ -1214,6 +1316,14 @@
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@babel/types@^7.29.7":
|
||||
version "7.29.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92"
|
||||
integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.29.7"
|
||||
"@babel/helper-validator-identifier" "^7.29.7"
|
||||
|
||||
"@braintree/sanitize-url@^7.1.1":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f"
|
||||
@@ -9532,12 +9642,12 @@ latest-version@^7.0.0:
|
||||
package-json "^8.1.0"
|
||||
|
||||
launch-editor@^2.6.1:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz"
|
||||
integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==
|
||||
version "2.14.1"
|
||||
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc"
|
||||
integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==
|
||||
dependencies:
|
||||
picocolors "^1.1.1"
|
||||
shell-quote "^1.8.3"
|
||||
shell-quote "^1.8.4"
|
||||
|
||||
layout-base@^1.0.0:
|
||||
version "1.0.2"
|
||||
@@ -13489,7 +13599,7 @@ shebang-regex@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.8.3:
|
||||
shell-quote@^1.8.4:
|
||||
version "1.8.4"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
|
||||
integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==
|
||||
|
||||
288
superset-embedded-sdk/package-lock.json
generated
288
superset-embedded-sdk/package-lock.json
generated
@@ -27,19 +27,6 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
|
||||
@@ -71,12 +58,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
@@ -85,32 +72,30 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
|
||||
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
|
||||
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
|
||||
"version": "7.29.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz",
|
||||
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/helper-compilation-targets": "^7.25.2",
|
||||
"@babel/helper-module-transforms": "^7.25.2",
|
||||
"@babel/helpers": "^7.25.0",
|
||||
"@babel/parser": "^7.25.0",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/traverse": "^7.25.2",
|
||||
"@babel/types": "^7.25.2",
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.6",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.29.2",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -126,13 +111,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -169,15 +154,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
|
||||
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
"@babel/helper-validator-option": "^7.24.8",
|
||||
"browserslist": "^4.23.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
},
|
||||
@@ -382,29 +366,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
|
||||
"integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -425,26 +408,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
|
||||
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/types": "^7.25.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
|
||||
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -1843,14 +1825,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1875,13 +1857,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2649,6 +2631,16 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
@@ -7983,16 +7975,6 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"@babel/cli": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.6.tgz",
|
||||
@@ -8011,38 +7993,38 @@
|
||||
}
|
||||
},
|
||||
"@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"@babel/compat-data": {
|
||||
"version": "7.25.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
|
||||
"integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/core": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
|
||||
"integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
|
||||
"version": "7.29.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.6.tgz",
|
||||
"integrity": "sha512-QdxmAo/ikZqqRGA8s43ww8lcql6naWRvEz0FFrl6MIlc7Gi6TroXnSdWa5U/kq6fzcpqpHesicQxFZIieZbyIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.25.0",
|
||||
"@babel/helper-compilation-targets": "^7.25.2",
|
||||
"@babel/helper-module-transforms": "^7.25.2",
|
||||
"@babel/helpers": "^7.25.0",
|
||||
"@babel/parser": "^7.25.0",
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/traverse": "^7.25.2",
|
||||
"@babel/types": "^7.25.2",
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.6",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.29.2",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
"gensync": "^1.0.0-beta.2",
|
||||
@@ -8051,13 +8033,13 @@
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
|
||||
"integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
@@ -8083,14 +8065,14 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-compilation-targets": {
|
||||
"version": "7.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
|
||||
"integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
"@babel/helper-validator-option": "^7.24.8",
|
||||
"browserslist": "^4.23.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
}
|
||||
@@ -8229,21 +8211,21 @@
|
||||
}
|
||||
},
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.24.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
|
||||
"integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-wrap-function": {
|
||||
@@ -8258,22 +8240,22 @@
|
||||
}
|
||||
},
|
||||
"@babel/helpers": {
|
||||
"version": "7.25.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
|
||||
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.25.0",
|
||||
"@babel/types": "^7.25.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.29.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
|
||||
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
|
||||
@@ -9157,14 +9139,14 @@
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
@@ -9183,13 +9165,13 @@
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@bcoe/v8-coverage": {
|
||||
@@ -9771,6 +9753,16 @@
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
|
||||
|
||||
39
superset-frontend/cypress-base/package-lock.json
generated
39
superset-frontend/cypress-base/package-lock.json
generated
@@ -4708,16 +4708,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -5029,10 +5028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
@@ -10228,7 +10226,7 @@
|
||||
"camelcase": "^5.3.1",
|
||||
"find-up": "^4.1.0",
|
||||
"get-package-type": "^0.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "^3.13.1",
|
||||
"resolve-from": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -10238,8 +10236,7 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"version": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -12345,15 +12342,15 @@
|
||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
}
|
||||
},
|
||||
"fromentries": {
|
||||
@@ -12574,9 +12571,9 @@
|
||||
}
|
||||
},
|
||||
"hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.2"
|
||||
}
|
||||
|
||||
46
superset-frontend/package-lock.json
generated
46
superset-frontend/package-lock.json
generated
@@ -11349,19 +11349,6 @@
|
||||
"integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
@@ -20682,22 +20669,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
@@ -26524,14 +26524,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz",
|
||||
"integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==",
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
|
||||
"integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picocolors": "^1.0.0",
|
||||
"shell-quote": "^1.8.1"
|
||||
"picocolors": "^1.1.1",
|
||||
"shell-quote": "^1.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/lerc": {
|
||||
|
||||
@@ -33,6 +33,7 @@ from superset.commands.database.exceptions import (
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelCreateFailedError,
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelInvalidError,
|
||||
)
|
||||
@@ -75,6 +76,7 @@ class CreateDatabaseCommand(BaseCommand):
|
||||
SupersetErrorsException,
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
) as ex:
|
||||
event_logger.log_with_context(
|
||||
action=f"db_creation_failed.{ex.__class__.__name__}",
|
||||
|
||||
@@ -75,3 +75,11 @@ class SSHTunnelMissingCredentials(CommandInvalidError, SSHTunnelError): # noqa:
|
||||
|
||||
class SSHTunnelInvalidCredentials(CommandInvalidError, SSHTunnelError): # noqa: N818
|
||||
message = _("Cannot have multiple credentials for the SSH Tunnel")
|
||||
|
||||
|
||||
class SSHTunnelHostKeyVerificationError(CommandInvalidError, SSHTunnelError):
|
||||
"""The SSH server's host key failed opt-in verification for a tunnel."""
|
||||
|
||||
message = _(
|
||||
"The SSH server host key could not be verified against the expected key."
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from superset.commands.database.exceptions import (
|
||||
)
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
SSHTunnelingNotEnabledError,
|
||||
)
|
||||
from superset.commands.database.utils import ping
|
||||
@@ -221,7 +222,11 @@ class TestConnectionDatabaseCommand(BaseCommand):
|
||||
engine=engine_name,
|
||||
)
|
||||
raise DatabaseSecurityUnsafeError(message=str(ex)) from ex
|
||||
except (SupersetTimeoutException, SSHTunnelingNotEnabledError) as ex:
|
||||
except (
|
||||
SupersetTimeoutException,
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
) as ex:
|
||||
event_logger.log_with_context(
|
||||
action=get_log_connection_action(
|
||||
"test_connection_error",
|
||||
@@ -230,7 +235,8 @@ class TestConnectionDatabaseCommand(BaseCommand):
|
||||
),
|
||||
engine=engine_name,
|
||||
)
|
||||
# bubble up the exception to return proper status code
|
||||
# bubble up the exception (preserving its specific message and status)
|
||||
# instead of flattening it into a generic connection failure
|
||||
raise
|
||||
except Exception as ex:
|
||||
if not database:
|
||||
|
||||
@@ -895,6 +895,15 @@ SSH_TUNNEL_TIMEOUT_SEC = 10.0
|
||||
#: Timeout (seconds) for transport socket (``socket.settimeout``)
|
||||
SSH_TUNNEL_PACKET_TIMEOUT_SEC = 1.0
|
||||
|
||||
#: Opt-in defense-in-depth: when enabled, every SSH tunnel must declare an expected
|
||||
#: server host key (``server_host_key`` on the tunnel) and the SSH server's presented
|
||||
#: host key is verified against it before the tunnel is opened. A mismatch, or a
|
||||
#: missing expected key while this flag is enabled, fails closed and the tunnel is
|
||||
#: rejected. When disabled (the default), tunnels without a ``server_host_key`` open
|
||||
#: without host-key verification, preserving existing behavior; tunnels that do set a
|
||||
#: ``server_host_key`` are still verified regardless of this flag.
|
||||
SSH_TUNNEL_STRICT_HOST_KEY_CHECKING: bool = False
|
||||
|
||||
|
||||
# Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars.
|
||||
DEFAULT_FEATURE_FLAGS.update(
|
||||
@@ -1722,9 +1731,14 @@ SMTP_USER = "superset"
|
||||
SMTP_PORT = 25
|
||||
SMTP_PASSWORD = "superset" # noqa: S105
|
||||
SMTP_MAIL_FROM = "superset@superset.com"
|
||||
# If True creates a default SSL context with ssl.Purpose.CLIENT_AUTH using the
|
||||
# default system root CA certificates.
|
||||
SMTP_SSL_SERVER_AUTH = False
|
||||
# If True creates a default SSL context with ssl.Purpose.SERVER_AUTH using the
|
||||
# default system root CA certificates. This makes STARTTLS/SSL connections to the
|
||||
# SMTP server validate the server's certificate against the trusted CA store.
|
||||
# Defaults to True so the mail server identity is verified out of the box. Set to
|
||||
# False to restore the previous behavior of skipping certificate validation (for
|
||||
# example, when using a self-signed certificate that is not in the system CA
|
||||
# store).
|
||||
SMTP_SSL_SERVER_AUTH = True
|
||||
ENABLE_CHUNK_ENCODING = False
|
||||
|
||||
# Whether to bump the logging level to ERROR on the flask_appbuilder package
|
||||
|
||||
@@ -56,6 +56,7 @@ from superset.commands.database.importers.dispatcher import ImportDatabasesComma
|
||||
from superset.commands.database.oauth2 import OAuth2StoreTokenCommand
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
SSHTunnelingNotEnabledError,
|
||||
)
|
||||
from superset.commands.database.sync_permissions import SyncPermissionsCommand
|
||||
@@ -484,7 +485,11 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||
exc_info=True,
|
||||
)
|
||||
return self.response_422(message=str(ex))
|
||||
except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
|
||||
except (
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
) as ex:
|
||||
return self.response_400(message=str(ex))
|
||||
except SupersetException as ex:
|
||||
return self.response(ex.status, message=ex.message)
|
||||
@@ -569,7 +574,11 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||
exc_info=True,
|
||||
)
|
||||
return self.response_422(message=str(ex))
|
||||
except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
|
||||
except (
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
) as ex:
|
||||
return self.response_400(message=str(ex))
|
||||
|
||||
@expose("/<int:pk>", methods=("DELETE",))
|
||||
@@ -1291,7 +1300,11 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||
try:
|
||||
TestConnectionDatabaseCommand(item).run()
|
||||
return self.response(200, message="OK")
|
||||
except (SSHTunnelingNotEnabledError, SSHTunnelDatabasePortError) as ex:
|
||||
except (
|
||||
SSHTunnelingNotEnabledError,
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
) as ex:
|
||||
return self.response_400(message=str(ex))
|
||||
|
||||
@expose("/<int:pk>/related_objects/", methods=("GET",))
|
||||
|
||||
@@ -477,6 +477,22 @@ class DatabaseSSHTunnel(Schema):
|
||||
private_key = fields.String(required=False, load_only=True)
|
||||
private_key_password = fields.String(required=False, load_only=True)
|
||||
|
||||
# Optional expected SSH server host key in authorized-key form
|
||||
# (e.g. "ssh-rsa AAAA...", "ssh-ed25519 AAAA..."). When set, the SSH server's
|
||||
# presented host key is verified against it before the tunnel is opened. This is
|
||||
# a public key, so it is not sensitive and is not masked.
|
||||
server_host_key = fields.String(
|
||||
required=False,
|
||||
allow_none=True,
|
||||
metadata={
|
||||
"description": (
|
||||
"Expected SSH server host key in authorized-key form "
|
||||
"(e.g. 'ssh-ed25519 AAAA...'). When set, the server's host key is "
|
||||
"verified against it before the tunnel is opened."
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@validates_schema
|
||||
def validate_authentication(self, data: dict[str, Any], **kwargs: Any) -> None:
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -72,6 +72,12 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
|
||||
encrypted_field_factory.create(Text), nullable=True
|
||||
)
|
||||
|
||||
# Optional expected SSH server host key, in authorized-key form
|
||||
# (e.g. "ssh-rsa AAAA...", "ssh-ed25519 AAAA..."). When set, the SSH server's
|
||||
# presented host key is verified against this value before the tunnel is opened.
|
||||
# This is a public key, so it is stored in plaintext (not encrypted).
|
||||
server_host_key = sa.Column(sa.Text, nullable=True)
|
||||
|
||||
export_fields = [
|
||||
"server_address",
|
||||
"server_port",
|
||||
@@ -79,6 +85,7 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
|
||||
"password",
|
||||
"private_key",
|
||||
"private_key_password",
|
||||
"server_host_key",
|
||||
]
|
||||
|
||||
extra_import_fields = [
|
||||
@@ -93,6 +100,9 @@ class SSHTunnel(AuditMixinNullable, ExtraJSONMixin, ImportExportMixin, Model):
|
||||
"server_port": self.server_port,
|
||||
"username": self.username,
|
||||
}
|
||||
if self.server_host_key is not None:
|
||||
# public key, not sensitive: returned in cleartext
|
||||
output["server_host_key"] = self.server_host_key
|
||||
if self.password is not None:
|
||||
output["password"] = PASSWORD_MASK
|
||||
if self.private_key is not None:
|
||||
|
||||
@@ -15,26 +15,63 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import logging
|
||||
import socket
|
||||
from io import StringIO
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import paramiko
|
||||
import sshtunnel
|
||||
from flask import Flask
|
||||
from paramiko import RSAKey
|
||||
from paramiko.pkey import UnknownKeyType
|
||||
|
||||
from superset.commands.database.ssh_tunnel.exceptions import SSHTunnelDatabasePortError
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelDatabasePortError,
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
)
|
||||
from superset.databases.utils import make_url_safe
|
||||
from superset.utils.class_utils import load_class_from_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.databases.ssh_tunnel.models import SSHTunnel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _parse_authorized_key(authorized_key: str) -> paramiko.PKey:
|
||||
"""
|
||||
Parse a host key in authorized-key form (``"<type> <base64>[ comment]"``) into a
|
||||
:class:`paramiko.PKey`. The optional trailing comment field and surrounding
|
||||
whitespace are ignored.
|
||||
|
||||
:raises ValueError: if the value is empty or cannot be parsed as a host key.
|
||||
"""
|
||||
fields = authorized_key.strip().split()
|
||||
if len(fields) < 2:
|
||||
raise ValueError("Host key must be in 'ssh-<type> <base64>' form")
|
||||
key_type, key_b64 = fields[0], fields[1]
|
||||
try:
|
||||
# validate=True so malformed characters raise instead of being silently
|
||||
# dropped, which could otherwise pin an unintended key value.
|
||||
key_bytes = base64.b64decode(key_b64, validate=True)
|
||||
except (binascii.Error, ValueError) as ex:
|
||||
raise ValueError("Host key base64 payload could not be decoded") from ex
|
||||
try:
|
||||
return paramiko.PKey.from_type_string(key_type, key_bytes)
|
||||
except (paramiko.SSHException, UnknownKeyType) as ex:
|
||||
raise ValueError(f"Host key could not be parsed: {ex}") from ex
|
||||
|
||||
|
||||
class SSHManager:
|
||||
def __init__(self, app: Flask) -> None:
|
||||
super().__init__()
|
||||
self.local_bind_address = app.config["SSH_TUNNEL_LOCAL_BIND_ADDRESS"]
|
||||
self.strict_host_key_checking = app.config.get(
|
||||
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING", False
|
||||
)
|
||||
sshtunnel.TUNNEL_TIMEOUT = app.config["SSH_TUNNEL_TIMEOUT_SEC"]
|
||||
sshtunnel.SSH_TIMEOUT = app.config["SSH_TUNNEL_PACKET_TIMEOUT_SEC"]
|
||||
|
||||
@@ -48,6 +85,87 @@ class SSHManager:
|
||||
port=server.local_bind_port,
|
||||
)
|
||||
|
||||
def _verify_host_key(self, ssh_tunnel: "SSHTunnel") -> "paramiko.PKey | None":
|
||||
"""
|
||||
Opt-in defense-in-depth: verify the SSH server's host key before opening the
|
||||
tunnel, to resist man-in-the-middle attacks (paramiko's ``Transport`` does no
|
||||
known-hosts checking by default).
|
||||
|
||||
Behavior:
|
||||
|
||||
- If the tunnel declares an expected ``server_host_key``, connect to the SSH
|
||||
server, read the host key it presents, and compare. On mismatch (or if the
|
||||
expected key cannot be parsed) raise
|
||||
:class:`SSHTunnelHostKeyVerificationError`.
|
||||
- If no expected key is set and ``SSH_TUNNEL_STRICT_HOST_KEY_CHECKING`` is
|
||||
enabled, fail closed and raise.
|
||||
- If no expected key is set and strict checking is disabled, do nothing,
|
||||
preserving existing (unverified) behavior.
|
||||
|
||||
:returns: the parsed expected host key when one is configured (so the caller
|
||||
can pin it on the tunnel's own connection), or ``None`` when no key is
|
||||
configured.
|
||||
"""
|
||||
expected_raw = ssh_tunnel.server_host_key
|
||||
|
||||
if not expected_raw or not expected_raw.strip():
|
||||
if self.strict_host_key_checking:
|
||||
raise SSHTunnelHostKeyVerificationError(
|
||||
message=(
|
||||
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING is enabled but no "
|
||||
"expected server host key is configured for this tunnel."
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
expected_key = _parse_authorized_key(expected_raw)
|
||||
except ValueError as ex:
|
||||
raise SSHTunnelHostKeyVerificationError(
|
||||
message=f"The configured expected server host key is invalid: {ex}"
|
||||
) from ex
|
||||
|
||||
# Build the socket ourselves with an explicit timeout so the TCP connect
|
||||
# phase is bounded too. ``paramiko.Transport((host, port))`` would connect
|
||||
# synchronously with no timeout, leaving ``start_client(timeout=...)`` to
|
||||
# govern only the SSH handshake; an unreachable host could then block for the
|
||||
# full OS-level TCP timeout.
|
||||
try:
|
||||
sock = socket.create_connection(
|
||||
(ssh_tunnel.server_address, ssh_tunnel.server_port),
|
||||
timeout=sshtunnel.SSH_TIMEOUT,
|
||||
)
|
||||
except OSError as ex:
|
||||
raise SSHTunnelHostKeyVerificationError(
|
||||
message=f"Could not connect to the SSH server: {ex}"
|
||||
) from ex
|
||||
|
||||
transport = paramiko.Transport(sock)
|
||||
try:
|
||||
transport.start_client(timeout=sshtunnel.SSH_TIMEOUT)
|
||||
remote_key = transport.get_remote_server_key()
|
||||
except Exception as ex: # noqa: BLE001
|
||||
raise SSHTunnelHostKeyVerificationError(
|
||||
message=f"Could not retrieve the SSH server host key: {ex}"
|
||||
) from ex
|
||||
finally:
|
||||
transport.close()
|
||||
|
||||
if remote_key != expected_key:
|
||||
logger.warning(
|
||||
"SSH host key mismatch for %s:%s",
|
||||
ssh_tunnel.server_address,
|
||||
ssh_tunnel.server_port,
|
||||
)
|
||||
raise SSHTunnelHostKeyVerificationError(
|
||||
message=(
|
||||
"The SSH server presented a host key that does not match the "
|
||||
"expected server host key configured for this tunnel."
|
||||
)
|
||||
)
|
||||
|
||||
return expected_key
|
||||
|
||||
def create_tunnel(
|
||||
self,
|
||||
ssh_tunnel: "SSHTunnel",
|
||||
@@ -60,6 +178,12 @@ class SSHManager:
|
||||
port = url.port or get_default_port(backend)
|
||||
if not port:
|
||||
raise SSHTunnelDatabasePortError()
|
||||
|
||||
# Opt-in host-key verification runs before the tunnel is opened. It returns
|
||||
# the parsed expected key (or None) so we can also pin it on the tunnel's own
|
||||
# connection below.
|
||||
expected_host_key = self._verify_host_key(ssh_tunnel)
|
||||
|
||||
params = {
|
||||
"ssh_address_or_host": (ssh_tunnel.server_address, ssh_tunnel.server_port),
|
||||
"ssh_username": ssh_tunnel.username,
|
||||
@@ -68,6 +192,14 @@ class SSHManager:
|
||||
"debug_level": logging.getLogger("flask_appbuilder").level,
|
||||
}
|
||||
|
||||
if expected_host_key is not None:
|
||||
# Pin the expected key on the tunnel's own connection, so paramiko verifies
|
||||
# the host that actually carries traffic on the same transport. The probe
|
||||
# above and the tunnel open separate connections, so verifying only the
|
||||
# probe would leave a TOCTOU gap (DNS re-resolution, selective
|
||||
# interception); pinning here closes it.
|
||||
params["ssh_host_key"] = expected_host_key
|
||||
|
||||
if ssh_tunnel.password:
|
||||
params["ssh_password"] = ssh_tunnel.password
|
||||
elif ssh_tunnel.private_key:
|
||||
|
||||
@@ -327,7 +327,16 @@ Chart Types You Can CREATE with generate_chart/generate_explore_link:
|
||||
- chart_type="xy", kind="scatter": Scatter plot for correlation analysis
|
||||
- chart_type="big_number": Big Number display (single metric, header only)
|
||||
- chart_type="big_number", show_trendline=True,
|
||||
temporal_column="<date_col>": Big Number with trendline
|
||||
temporal_column="<date_col>", aggregation="sum": Big Number with trendline
|
||||
(aggregation controls how the value is computed from trendline data points;
|
||||
default when omitted is "LAST_VALUE" — most recent point only.
|
||||
Use aggregation="sum" for all-time totals, "mean" for averages, "max"/"min" for extremes.
|
||||
DIAGNOSIS: if a Big Number with Trendline shows wrong values, check
|
||||
form_data["aggregation"] — missing/LAST_VALUE means the chart shows only the last data
|
||||
point, not a total. Fix by calling update_chart with a complete Big Number config:
|
||||
chart_type="big_number", metric=<metric>, show_trendline=True,
|
||||
temporal_column=<date_col>, aggregation="sum". update_chart requires the full
|
||||
config — omitting chart_type or metric causes a validation error.)
|
||||
- chart_type="table": Data table for detailed views
|
||||
- chart_type="table", viz_type="ag-grid-table": Interactive AG Grid table
|
||||
- chart_type="pie": Pie chart for proportional data (set donut=True for donut)
|
||||
|
||||
@@ -859,6 +859,9 @@ def map_big_number_config(config: BigNumberChartConfig) -> Dict[str, Any]:
|
||||
if config.time_format:
|
||||
form_data["time_format"] = config.time_format
|
||||
|
||||
if config.aggregation is not None:
|
||||
form_data["aggregation"] = config.aggregation
|
||||
|
||||
_add_adhoc_filters(form_data, config.filters)
|
||||
|
||||
return form_data
|
||||
|
||||
@@ -1417,6 +1417,32 @@ class BigNumberChartConfig(UnknownFieldCheckMixin):
|
||||
),
|
||||
ge=1,
|
||||
)
|
||||
aggregation: (
|
||||
Literal["LAST_VALUE", "sum", "mean", "min", "max", "median", "raw"] | None
|
||||
) = Field(
|
||||
None,
|
||||
description=(
|
||||
"How the single big-number value is computed from the trendline "
|
||||
"data points. Only applies when show_trendline=True. "
|
||||
"Options: "
|
||||
"'sum' = Total (Sum) — add all data points; use for all-time totals. "
|
||||
"'LAST_VALUE' = most recent data point "
|
||||
"(frontend default when this field is absent). "
|
||||
"'mean' = Average (Mean). "
|
||||
"'min' = Minimum. "
|
||||
"'max' = Maximum. "
|
||||
"'median' = Median. "
|
||||
"'raw' = Overall value — single aggregate across the full period; best for "
|
||||
"non-additive metrics like ratios, averages, or distinct counts. "
|
||||
"DIAGNOSIS: if a Big Number with Trendline shows an unexpectedly low value "
|
||||
"(e.g. yesterday's revenue instead of all-time total), "
|
||||
"inspect form_data['aggregation'] "
|
||||
"— when absent or 'LAST_VALUE' the chart shows only the last data point. "
|
||||
"Fix by setting aggregation='sum'. "
|
||||
"IMPORTANT: when updating aggregation, always include "
|
||||
"show_trendline=True and temporal_column to preserve the trendline."
|
||||
),
|
||||
)
|
||||
filters: list[FilterConfig] | None = Field(
|
||||
None,
|
||||
description="Filters to apply",
|
||||
@@ -1437,6 +1463,13 @@ class BigNumberChartConfig(UnknownFieldCheckMixin):
|
||||
"Period comparison is only available for "
|
||||
"trendline charts."
|
||||
)
|
||||
if self.aggregation and not self.show_trendline:
|
||||
raise ValueError(
|
||||
"aggregation requires show_trendline=True. "
|
||||
"The aggregation field only applies to Big Number with "
|
||||
"Trendline charts. Set show_trendline=True and provide "
|
||||
"a temporal_column, or omit aggregation."
|
||||
)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -115,6 +115,14 @@ _CHART_EXAMPLES: Dict[str, list[Dict[str, Any]]] = {
|
||||
"chart_type": "big_number",
|
||||
"metric": {"name": "revenue", "aggregate": "SUM"},
|
||||
},
|
||||
{
|
||||
"chart_type": "big_number",
|
||||
"metric": {"name": "revenue", "aggregate": "SUM"},
|
||||
"temporal_column": "order_date",
|
||||
"show_trendline": True,
|
||||
"aggregation": "sum",
|
||||
"time_grain": "P1D",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from flask import current_app
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import lazyload, Session
|
||||
|
||||
from superset import db, security_manager
|
||||
from superset.db_engine_specs.base import GenericDBException
|
||||
@@ -379,7 +379,15 @@ def upgrade_catalog_perms(engines: set[str] | None = None) -> None:
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
|
||||
for database in session.query(Database).all():
|
||||
# The Database model has an eager-loaded (``lazy="joined"``) ``ssh_tunnel``
|
||||
# backref. Eager-loading it here would SELECT every column on ``ssh_tunnels``,
|
||||
# including columns added by later migrations that do not yet exist at the
|
||||
# revision this helper runs in (e.g. on a fresh DB upgraded in one pass). The
|
||||
# catalog upgrade only needs scalar ``Database`` columns, so disable the eager
|
||||
# join to keep the query schema-safe across migration revisions.
|
||||
for database in (
|
||||
session.query(Database).options(lazyload(Database.ssh_tunnel)).all()
|
||||
):
|
||||
db_engine_spec = database.db_engine_spec
|
||||
if (
|
||||
engines and db_engine_spec.engine not in engines
|
||||
@@ -576,7 +584,11 @@ def downgrade_catalog_perms(engines: set[str] | None = None) -> None:
|
||||
bind = op.get_bind()
|
||||
session = db.Session(bind=bind)
|
||||
|
||||
for database in session.query(Database).all():
|
||||
# See upgrade_catalog_perms: avoid eager-loading the ``ssh_tunnel`` backref so the
|
||||
# query stays schema-safe across migration revisions.
|
||||
for database in (
|
||||
session.query(Database).options(lazyload(Database.ssh_tunnel)).all()
|
||||
):
|
||||
db_engine_spec = database.db_engine_spec
|
||||
if (
|
||||
engines and db_engine_spec.engine not in engines
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""add server_host_key to ssh_tunnels
|
||||
|
||||
Adds a nullable ``server_host_key`` column to the ``ssh_tunnels`` table. It stores the
|
||||
expected SSH server host key in authorized-key form (e.g. "ssh-ed25519 AAAA...") so
|
||||
operators can opt in to verifying the SSH server's host key before a tunnel is opened.
|
||||
This is a public key and is stored in plaintext (not encrypted). The column is
|
||||
nullable, so existing tunnels are unaffected.
|
||||
|
||||
Revision ID: 78a40c08b4be
|
||||
Revises: b7c9d1e2f3a4
|
||||
Create Date: 2026-06-03 10:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from superset.migrations.shared.utils import add_columns, drop_columns
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "78a40c08b4be"
|
||||
down_revision = "b7c9d1e2f3a4"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add the nullable ``server_host_key`` column to ``ssh_tunnels``."""
|
||||
add_columns(
|
||||
"ssh_tunnels",
|
||||
sa.Column("server_host_key", sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Drop the ``server_host_key`` column from ``ssh_tunnels``."""
|
||||
drop_columns("ssh_tunnels", "server_host_key")
|
||||
@@ -37,9 +37,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestEmailSmtp(SupersetTestCase):
|
||||
def setUp(self):
|
||||
SMTP_CONFIG_KEYS = ("SMTP_SSL", "SMTP_SSL_SERVER_AUTH", "SMTP_STARTTLS")
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._original_smtp_config = {
|
||||
key: current_app.config[key] for key in self.SMTP_CONFIG_KEYS
|
||||
}
|
||||
current_app.config["SMTP_SSL"] = False
|
||||
|
||||
def tearDown(self) -> None:
|
||||
current_app.config.update(self._original_smtp_config)
|
||||
super().tearDown()
|
||||
|
||||
@mock.patch("superset.utils.core.send_mime_email")
|
||||
def test_send_smtp(self, mock_send_mime):
|
||||
attachment = tempfile.NamedTemporaryFile()
|
||||
@@ -208,6 +217,7 @@ class TestEmailSmtp(SupersetTestCase):
|
||||
@mock.patch("smtplib.SMTP")
|
||||
def test_send_mime_ssl(self, mock_smtp, mock_smtp_ssl):
|
||||
current_app.config["SMTP_SSL"] = True
|
||||
current_app.config["SMTP_SSL_SERVER_AUTH"] = False
|
||||
mock_smtp.return_value = mock.Mock()
|
||||
mock_smtp_ssl.return_value = mock.Mock()
|
||||
utils.send_mime_email(
|
||||
|
||||
@@ -312,3 +312,123 @@ def test_full_setting(
|
||||
assert dttm_col.is_dttm
|
||||
assert dttm_col.python_date_format == "epoch_s"
|
||||
assert dttm_col.expression == "CAST(dttm as INTEGER)"
|
||||
|
||||
|
||||
def test_smtp_ssl_server_auth_defaults_to_true() -> None:
|
||||
"""
|
||||
The shipped default for SMTP_SSL_SERVER_AUTH validates the SMTP server's
|
||||
TLS certificate. Operators can still opt out by overriding it to False.
|
||||
"""
|
||||
from superset import config
|
||||
|
||||
assert config.SMTP_SSL_SERVER_AUTH is True
|
||||
|
||||
|
||||
def _smtp_config(**overrides: Any) -> dict[str, Any]:
|
||||
"""
|
||||
Build a minimal SMTP config dict for ``send_mime_email`` tests, with
|
||||
plaintext transport defaults; keyword ``overrides`` replace any key.
|
||||
"""
|
||||
config = {
|
||||
"SMTP_HOST": "localhost",
|
||||
"SMTP_PORT": 25,
|
||||
"SMTP_USER": "",
|
||||
"SMTP_PASSWORD": "",
|
||||
"SMTP_STARTTLS": False,
|
||||
"SMTP_SSL": False,
|
||||
"SMTP_SSL_SERVER_AUTH": True,
|
||||
}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def test_send_mime_email_ssl_server_auth_passes_context(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""
|
||||
With SMTP_SSL and SMTP_SSL_SERVER_AUTH enabled, ``send_mime_email`` builds a
|
||||
default SSL context and threads it through to ``smtplib.SMTP_SSL`` so the
|
||||
server certificate is validated.
|
||||
"""
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
from superset.utils import core as utils
|
||||
|
||||
create_default_context = mocker.patch(
|
||||
"superset.utils.core.ssl.create_default_context"
|
||||
)
|
||||
smtp_ssl = mocker.patch("smtplib.SMTP_SSL")
|
||||
smtp = mocker.patch("smtplib.SMTP")
|
||||
|
||||
utils.send_mime_email(
|
||||
"from",
|
||||
["to"],
|
||||
MIMEMultipart(),
|
||||
_smtp_config(SMTP_SSL=True, SMTP_SSL_SERVER_AUTH=True),
|
||||
dryrun=False,
|
||||
)
|
||||
|
||||
create_default_context.assert_called_once_with()
|
||||
assert not smtp.called
|
||||
smtp_ssl.assert_called_once_with(
|
||||
"localhost", 25, context=create_default_context.return_value
|
||||
)
|
||||
|
||||
|
||||
def test_send_mime_email_starttls_server_auth_passes_context(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""
|
||||
With STARTTLS and SMTP_SSL_SERVER_AUTH enabled, ``send_mime_email`` builds a
|
||||
default SSL context and threads it through to ``starttls`` so the server
|
||||
certificate is validated.
|
||||
"""
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
from superset.utils import core as utils
|
||||
|
||||
create_default_context = mocker.patch(
|
||||
"superset.utils.core.ssl.create_default_context"
|
||||
)
|
||||
smtp = mocker.patch("smtplib.SMTP")
|
||||
|
||||
utils.send_mime_email(
|
||||
"from",
|
||||
["to"],
|
||||
MIMEMultipart(),
|
||||
_smtp_config(SMTP_STARTTLS=True, SMTP_SSL_SERVER_AUTH=True),
|
||||
dryrun=False,
|
||||
)
|
||||
|
||||
create_default_context.assert_called_once_with()
|
||||
smtp.return_value.starttls.assert_called_once_with(
|
||||
context=create_default_context.return_value
|
||||
)
|
||||
|
||||
|
||||
def test_send_mime_email_server_auth_disabled_skips_context(
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""
|
||||
When SMTP_SSL_SERVER_AUTH is disabled no SSL context is built and ``None`` is
|
||||
passed through, preserving the opt-out (certificate validation skipped).
|
||||
"""
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
from superset.utils import core as utils
|
||||
|
||||
create_default_context = mocker.patch(
|
||||
"superset.utils.core.ssl.create_default_context"
|
||||
)
|
||||
smtp_ssl = mocker.patch("smtplib.SMTP_SSL")
|
||||
|
||||
utils.send_mime_email(
|
||||
"from",
|
||||
["to"],
|
||||
MIMEMultipart(),
|
||||
_smtp_config(SMTP_SSL=True, SMTP_SSL_SERVER_AUTH=False),
|
||||
dryrun=False,
|
||||
)
|
||||
|
||||
assert not create_default_context.called
|
||||
smtp_ssl.assert_called_once_with("localhost", 25, context=None)
|
||||
|
||||
@@ -119,5 +119,5 @@ def test_database_filter(mocker: MockerFixture) -> None:
|
||||
)
|
||||
assert (
|
||||
str(compiled_query)
|
||||
== "SELECT dbs.uuid, dbs.created_on, dbs.changed_on, dbs.id, dbs.verbose_name, dbs.database_name, dbs.sqlalchemy_uri, dbs.password, dbs.cache_timeout, dbs.select_as_create_table_as, dbs.expose_in_sqllab, dbs.configuration_method, dbs.allow_run_async, dbs.allow_file_upload, dbs.allow_ctas, dbs.allow_cvas, dbs.allow_dml, dbs.force_ctas_schema, dbs.extra, dbs.encrypted_extra, dbs.impersonate_user, dbs.server_cert, dbs.is_managed_externally, dbs.external_url, dbs.created_by_fk, dbs.changed_by_fk, ssh_tunnels_1.uuid AS uuid_1, ssh_tunnels_1.created_on AS created_on_1, ssh_tunnels_1.changed_on AS changed_on_1, ssh_tunnels_1.extra_json, ssh_tunnels_1.id AS id_1, ssh_tunnels_1.database_id, ssh_tunnels_1.server_address, ssh_tunnels_1.server_port, ssh_tunnels_1.username, ssh_tunnels_1.password AS password_1, ssh_tunnels_1.private_key, ssh_tunnels_1.private_key_password, ssh_tunnels_1.created_by_fk AS created_by_fk_1, ssh_tunnels_1.changed_by_fk AS changed_by_fk_1 \nFROM dbs LEFT OUTER JOIN ssh_tunnels AS ssh_tunnels_1 ON dbs.id = ssh_tunnels_1.database_id \nWHERE '[' || dbs.database_name || '].(id:' || CAST(dbs.id AS VARCHAR) || ')' IN ('[my_db].(id:42)', '[my_other_db].(id:43)') OR dbs.database_name IN ('my_db', 'my_other_db', 'third_db')" # noqa: E501
|
||||
== "SELECT dbs.uuid, dbs.created_on, dbs.changed_on, dbs.id, dbs.verbose_name, dbs.database_name, dbs.sqlalchemy_uri, dbs.password, dbs.cache_timeout, dbs.select_as_create_table_as, dbs.expose_in_sqllab, dbs.configuration_method, dbs.allow_run_async, dbs.allow_file_upload, dbs.allow_ctas, dbs.allow_cvas, dbs.allow_dml, dbs.force_ctas_schema, dbs.extra, dbs.encrypted_extra, dbs.impersonate_user, dbs.server_cert, dbs.is_managed_externally, dbs.external_url, dbs.created_by_fk, dbs.changed_by_fk, ssh_tunnels_1.uuid AS uuid_1, ssh_tunnels_1.created_on AS created_on_1, ssh_tunnels_1.changed_on AS changed_on_1, ssh_tunnels_1.extra_json, ssh_tunnels_1.id AS id_1, ssh_tunnels_1.database_id, ssh_tunnels_1.server_address, ssh_tunnels_1.server_port, ssh_tunnels_1.username, ssh_tunnels_1.password AS password_1, ssh_tunnels_1.private_key, ssh_tunnels_1.private_key_password, ssh_tunnels_1.server_host_key, ssh_tunnels_1.created_by_fk AS created_by_fk_1, ssh_tunnels_1.changed_by_fk AS changed_by_fk_1 \nFROM dbs LEFT OUTER JOIN ssh_tunnels AS ssh_tunnels_1 ON dbs.id = ssh_tunnels_1.database_id \nWHERE '[' || dbs.database_name || '].(id:' || CAST(dbs.id AS VARCHAR) || ')' IN ('[my_db].(id:42)', '[my_other_db].(id:43)') OR dbs.database_name IN ('my_db', 'my_other_db', 'third_db')" # noqa: E501
|
||||
)
|
||||
|
||||
@@ -14,11 +14,44 @@
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import paramiko
|
||||
import pytest
|
||||
import sshtunnel
|
||||
|
||||
from superset.extensions.ssh import SSHManagerFactory
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
)
|
||||
from superset.extensions.ssh import SSHManager, SSHManagerFactory
|
||||
|
||||
|
||||
def _make_manager(strict: bool = False) -> SSHManager:
|
||||
"""Build an ``SSHManager`` test instance with configurable strict checking."""
|
||||
app = Mock()
|
||||
app.config = {
|
||||
"SSH_TUNNEL_MAX_RETRIES": 2,
|
||||
"SSH_TUNNEL_LOCAL_BIND_ADDRESS": "127.0.0.1",
|
||||
"SSH_TUNNEL_TIMEOUT_SEC": 123.0,
|
||||
"SSH_TUNNEL_PACKET_TIMEOUT_SEC": 321.0,
|
||||
"SSH_TUNNEL_MANAGER_CLASS": "superset.extensions.ssh.SSHManager",
|
||||
"SSH_TUNNEL_STRICT_HOST_KEY_CHECKING": strict,
|
||||
}
|
||||
return SSHManager(app)
|
||||
|
||||
|
||||
def _authorized_key(key: paramiko.PKey) -> str:
|
||||
"""Render a paramiko key in authorized-key (``"<type> <base64>"``) form."""
|
||||
return f"{key.get_name()} {key.get_base64()}"
|
||||
|
||||
|
||||
def _ssh_tunnel(server_host_key: str | None) -> Mock:
|
||||
"""Create a mocked SSH tunnel with server connection fields populated."""
|
||||
tunnel = Mock()
|
||||
tunnel.server_address = "ssh.example.com"
|
||||
tunnel.server_port = 22
|
||||
tunnel.server_host_key = server_host_key
|
||||
return tunnel
|
||||
|
||||
|
||||
def test_ssh_tunnel_timeout_setting() -> None:
|
||||
@@ -34,3 +67,199 @@ def test_ssh_tunnel_timeout_setting() -> None:
|
||||
factory.init_app(app)
|
||||
assert sshtunnel.TUNNEL_TIMEOUT == 123.0
|
||||
assert sshtunnel.SSH_TIMEOUT == 321.0
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_match(
|
||||
mock_transport_cls: Mock, mock_create_connection: Mock
|
||||
) -> None:
|
||||
"""The server presents the same key we expect: verification passes."""
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel(_authorized_key(server_key))
|
||||
|
||||
transport = mock_transport_cls.return_value
|
||||
transport.get_remote_server_key.return_value = server_key
|
||||
|
||||
result = manager._verify_host_key(tunnel) # should not raise
|
||||
|
||||
# The TCP connect is bounded by an explicit timeout, and the resulting
|
||||
# socket is handed to Transport.
|
||||
mock_create_connection.assert_called_once_with(
|
||||
("ssh.example.com", 22), timeout=321.0
|
||||
)
|
||||
mock_transport_cls.assert_called_once_with(mock_create_connection.return_value)
|
||||
transport.start_client.assert_called_once()
|
||||
transport.close.assert_called_once()
|
||||
# The parsed expected key is returned so the caller can pin it on the tunnel.
|
||||
assert result == server_key
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_mismatch_raises(
|
||||
mock_transport_cls: Mock, mock_create_connection: Mock
|
||||
) -> None:
|
||||
"""The server presents a different key than expected: verification fails."""
|
||||
expected_key = paramiko.RSAKey.generate(2048)
|
||||
presented_key = paramiko.RSAKey.generate(2048)
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel(_authorized_key(expected_key))
|
||||
|
||||
transport = mock_transport_cls.return_value
|
||||
transport.get_remote_server_key.return_value = presented_key
|
||||
|
||||
with pytest.raises(SSHTunnelHostKeyVerificationError):
|
||||
manager._verify_host_key(tunnel)
|
||||
|
||||
mock_create_connection.assert_called_once()
|
||||
transport.close.assert_called_once()
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
def test_verify_host_key_connect_failure_raises(
|
||||
mock_create_connection: Mock,
|
||||
) -> None:
|
||||
"""A bounded TCP connect failure surfaces as a host-key verification error."""
|
||||
manager = _make_manager(strict=False)
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
tunnel = _ssh_tunnel(_authorized_key(server_key))
|
||||
|
||||
mock_create_connection.side_effect = OSError("connection refused")
|
||||
|
||||
with pytest.raises(SSHTunnelHostKeyVerificationError):
|
||||
manager._verify_host_key(tunnel)
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_unset_non_strict_skips(mock_transport_cls: Mock) -> None:
|
||||
"""Back-compat: no expected key + strict checking off => no verification at all."""
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel(None)
|
||||
|
||||
assert manager._verify_host_key(tunnel) is None # should not raise
|
||||
|
||||
mock_transport_cls.assert_not_called()
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_unset_strict_raises(mock_transport_cls: Mock) -> None:
|
||||
"""Fail-closed: no expected key + strict checking on => reject."""
|
||||
manager = _make_manager(strict=True)
|
||||
tunnel = _ssh_tunnel(None)
|
||||
|
||||
with pytest.raises(SSHTunnelHostKeyVerificationError):
|
||||
manager._verify_host_key(tunnel)
|
||||
|
||||
mock_transport_cls.assert_not_called()
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_match_ignores_comment_and_whitespace(
|
||||
mock_transport_cls: Mock,
|
||||
mock_create_connection: Mock,
|
||||
) -> None:
|
||||
# The stored key may carry a trailing comment and extra whitespace.
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
manager = _make_manager(strict=False)
|
||||
stored = f" {_authorized_key(server_key)} user@host "
|
||||
tunnel = _ssh_tunnel(stored)
|
||||
|
||||
transport = mock_transport_cls.return_value
|
||||
transport.get_remote_server_key.return_value = server_key
|
||||
|
||||
manager._verify_host_key(tunnel) # should not raise
|
||||
|
||||
# Whitespace/comment stripping must not short-circuit verification: the
|
||||
# bounded TCP connect and Transport handshake still run as in the plain
|
||||
# match case.
|
||||
mock_create_connection.assert_called_once_with(
|
||||
("ssh.example.com", 22), timeout=321.0
|
||||
)
|
||||
mock_transport_cls.assert_called_once_with(mock_create_connection.return_value)
|
||||
transport.start_client.assert_called_once()
|
||||
transport.close.assert_called_once()
|
||||
|
||||
|
||||
def test_verify_host_key_invalid_expected_raises() -> None:
|
||||
# A malformed expected key is rejected before any network connection.
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel("not-a-valid-key")
|
||||
|
||||
with pytest.raises(SSHTunnelHostKeyVerificationError):
|
||||
manager._verify_host_key(tunnel)
|
||||
|
||||
|
||||
def test_verify_host_key_unknown_key_type_raises() -> None:
|
||||
"""An unsupported key type is wrapped in the verification error, not leaked."""
|
||||
manager = _make_manager(strict=False)
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
tunnel = _ssh_tunnel(f"ssh-bogus {server_key.get_base64()}")
|
||||
|
||||
with pytest.raises(SSHTunnelHostKeyVerificationError):
|
||||
manager._verify_host_key(tunnel)
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.sshtunnel.open_tunnel")
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_create_tunnel_pins_verified_host_key(
|
||||
mock_transport_cls: Mock,
|
||||
mock_create_connection: Mock,
|
||||
mock_open_tunnel: Mock,
|
||||
) -> None:
|
||||
"""A verified expected key is also pinned on the tunnel's own connection.
|
||||
|
||||
When an expected host key is configured and verified, it is also pinned on the
|
||||
tunnel's own connection (``ssh_host_key``) so paramiko verifies the host that
|
||||
actually carries traffic on the same transport — closing the probe-vs-tunnel
|
||||
TOCTOU gap rather than trusting only the pre-flight probe.
|
||||
"""
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel(_authorized_key(server_key))
|
||||
tunnel.username = "user"
|
||||
tunnel.password = None
|
||||
tunnel.private_key = None
|
||||
|
||||
mock_transport_cls.return_value.get_remote_server_key.return_value = server_key
|
||||
|
||||
manager.create_tunnel(tunnel, "postgresql://u:p@db:5432/ex")
|
||||
|
||||
_, kwargs = mock_open_tunnel.call_args
|
||||
assert kwargs["ssh_host_key"] == server_key
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.sshtunnel.open_tunnel")
|
||||
def test_create_tunnel_without_host_key_does_not_pin(mock_open_tunnel: Mock) -> None:
|
||||
# No expected key configured (non-strict): nothing is pinned, preserving the
|
||||
# prior behavior.
|
||||
manager = _make_manager(strict=False)
|
||||
tunnel = _ssh_tunnel(None)
|
||||
tunnel.username = "user"
|
||||
tunnel.password = None
|
||||
tunnel.private_key = None
|
||||
|
||||
manager.create_tunnel(tunnel, "postgresql://u:p@db:5432/ex")
|
||||
|
||||
_, kwargs = mock_open_tunnel.call_args
|
||||
assert "ssh_host_key" not in kwargs
|
||||
|
||||
|
||||
def test_ssh_tunnel_schema_round_trips_server_host_key() -> None:
|
||||
"""The schema accepts and preserves the public host key field."""
|
||||
from superset.databases.schemas import DatabaseSSHTunnel
|
||||
|
||||
server_key = paramiko.RSAKey.generate(2048)
|
||||
authorized = _authorized_key(server_key)
|
||||
payload = {
|
||||
"server_address": "ssh.example.com",
|
||||
"server_port": 22,
|
||||
"username": "user",
|
||||
"password": "secret",
|
||||
"server_host_key": authorized,
|
||||
}
|
||||
loaded = DatabaseSSHTunnel().load(payload)
|
||||
assert loaded["server_host_key"] == authorized
|
||||
|
||||
@@ -177,6 +177,17 @@ class TestBigNumberChartConfig:
|
||||
compare_lag=1,
|
||||
)
|
||||
|
||||
def test_aggregation_requires_trendline(self) -> None:
|
||||
"""aggregation without show_trendline=True must raise ValidationError."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="aggregation requires show_trendline"
|
||||
):
|
||||
BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
aggregation="sum",
|
||||
)
|
||||
|
||||
def test_with_filters(self) -> None:
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
@@ -188,6 +199,36 @@ class TestBigNumberChartConfig:
|
||||
assert config.filters is not None
|
||||
assert len(config.filters) == 1
|
||||
|
||||
def test_with_aggregation_sum(self) -> None:
|
||||
"""aggregation='sum' is accepted when show_trendline=True."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
temporal_column="ds",
|
||||
show_trendline=True,
|
||||
aggregation="sum",
|
||||
)
|
||||
assert config.aggregation == "sum"
|
||||
|
||||
def test_with_aggregation_last_value(self) -> None:
|
||||
"""aggregation='LAST_VALUE' is accepted when show_trendline=True."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
temporal_column="ds",
|
||||
show_trendline=True,
|
||||
aggregation="LAST_VALUE",
|
||||
)
|
||||
assert config.aggregation == "LAST_VALUE"
|
||||
|
||||
def test_aggregation_defaults_to_none(self) -> None:
|
||||
"""aggregation field is None when omitted."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
)
|
||||
assert config.aggregation is None
|
||||
|
||||
def test_extra_fields_forbidden(self) -> None:
|
||||
with pytest.raises(ValueError, match="Unknown field 'unknown_field'"):
|
||||
BigNumberChartConfig(
|
||||
@@ -307,6 +348,52 @@ class TestMapBigNumberConfig:
|
||||
assert "time_grain_sqla" not in form_data
|
||||
assert "start_y_axis_at_zero" not in form_data
|
||||
|
||||
def test_with_aggregation_sum(self) -> None:
|
||||
"""aggregation='sum' is written to form_data for trendline charts."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
temporal_column="order_date",
|
||||
show_trendline=True,
|
||||
aggregation="sum",
|
||||
)
|
||||
form_data = map_big_number_config(config)
|
||||
assert form_data["aggregation"] == "sum"
|
||||
|
||||
def test_with_aggregation_last_value(self) -> None:
|
||||
"""aggregation='LAST_VALUE' is written to form_data for trendline charts."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
temporal_column="order_date",
|
||||
show_trendline=True,
|
||||
aggregation="LAST_VALUE",
|
||||
)
|
||||
form_data = map_big_number_config(config)
|
||||
assert form_data["aggregation"] == "LAST_VALUE"
|
||||
|
||||
def test_aggregation_absent_when_not_set(self) -> None:
|
||||
"""aggregation key is absent from form_data when config.aggregation is None."""
|
||||
config = BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
temporal_column="order_date",
|
||||
show_trendline=True,
|
||||
)
|
||||
form_data = map_big_number_config(config)
|
||||
assert "aggregation" not in form_data
|
||||
|
||||
def test_aggregation_not_allowed_for_big_number_total(self) -> None:
|
||||
"""aggregation is rejected when show_trendline=False (big_number_total)."""
|
||||
with pytest.raises(
|
||||
ValidationError, match="aggregation requires show_trendline"
|
||||
):
|
||||
BigNumberChartConfig(
|
||||
chart_type="big_number",
|
||||
metric=ColumnRef(name="revenue", aggregate="SUM"),
|
||||
aggregation="sum",
|
||||
)
|
||||
|
||||
|
||||
class TestMapConfigToFormDataBigNumber:
|
||||
"""Test map_config_to_form_data dispatch for big number."""
|
||||
|
||||
Reference in New Issue
Block a user