queryapigate 0.28.0__tar.gz → 0.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {queryapigate-0.28.0/queryapigate.egg-info → queryapigate-0.30.0}/PKG-INFO +7 -5
- {queryapigate-0.28.0 → queryapigate-0.30.0}/README.md +2 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/pyproject.toml +6 -4
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/__init__.py +1 -1
- queryapigate-0.30.0/queryapigate/accesslog.py +29 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/adminroles.py +3 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/app.py +39 -7
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cli.py +34 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/config.py +49 -3
- queryapigate-0.28.0/queryapigate/console_dist/assets/AccessMapPage-DIu936ZH.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AccessMapPage-CU9b_-fo.js +3 -3
- queryapigate-0.28.0/queryapigate/console_dist/assets/AdministratorsPage-B2IYmP_x.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AdministratorsPage-Bwjtg36U.js +1 -1
- queryapigate-0.30.0/queryapigate/console_dist/assets/AgentsPage-BmXNCpQ0.js +1 -0
- queryapigate-0.28.0/queryapigate/console_dist/assets/AlertsPage-CzC-qhMh.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AlertsPage-C7tvw3Hr.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/ApiKeysPage-C0VW5NrS.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ApiKeysPage-B-TKdW-V.js +1 -1
- queryapigate-0.30.0/queryapigate/console_dist/assets/ApprovalsPage-Bqsr5dHt.js +1 -0
- queryapigate-0.28.0/queryapigate/console_dist/assets/AuditLogPage-GdbYovis.js → queryapigate-0.30.0/queryapigate/console_dist/assets/AuditLogPage-hAKiebHz.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/CachingPage-4stBdbWQ.js → queryapigate-0.30.0/queryapigate/console_dist/assets/CachingPage-WFUpWs_W.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/CodeBox-DRfv6yXf.js → queryapigate-0.30.0/queryapigate/console_dist/assets/CodeBox-DuaKuoNl.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/ConnectionsPage-XYubobx_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ConnectionsPage-DwdiiWsf.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/DesignerPage-B1Sc83US.js → queryapigate-0.30.0/queryapigate/console_dist/assets/DesignerPage-BfhC69pJ.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/DestinationsPage-C30f018_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/DestinationsPage-yaGxeOXB.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/ExportsPage-BMRj3VOx.js → queryapigate-0.30.0/queryapigate/console_dist/assets/ExportsPage-DlkNfYMZ.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/HelpPage-DCkoTqa5.js → queryapigate-0.30.0/queryapigate/console_dist/assets/HelpPage-Bey5KI_-.js +2 -2
- queryapigate-0.28.0/queryapigate/console_dist/assets/HomePage-CcpGDBK_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/HomePage-DeF8Zaq6.js +2 -2
- queryapigate-0.28.0/queryapigate/console_dist/assets/Managed-CNWDZi6e.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Managed-CQVNTWrU.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/MetricsPage-Dxdebop4.js → queryapigate-0.30.0/queryapigate/console_dist/assets/MetricsPage-DqeoN2tZ.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/RepositoryPage-DmazL70u.js → queryapigate-0.30.0/queryapigate/console_dist/assets/RepositoryPage-QqouaqLm.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/Results-BdoTFMC_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Results-BhwS6Q_T.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/RolesPage-Dn-c3szF.js → queryapigate-0.30.0/queryapigate/console_dist/assets/RolesPage-DhkAkZ1v.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/SchemaBrowser-mmFZRlm_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/SchemaBrowser-DQ0mi4Vm.js +1 -1
- queryapigate-0.30.0/queryapigate/console_dist/assets/SettingsPage-4T0vN6dU.js +3 -0
- queryapigate-0.28.0/queryapigate/console_dist/assets/StatTile-DnMgkKp4.js → queryapigate-0.30.0/queryapigate/console_dist/assets/StatTile-DHKvOsXg.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/Time-CZQinVk_.js → queryapigate-0.30.0/queryapigate/console_dist/assets/Time-KeJDv2te.js +1 -1
- queryapigate-0.30.0/queryapigate/console_dist/assets/forms-StJt-xsd.js +1 -0
- queryapigate-0.28.0/queryapigate/console_dist/assets/forms-BlZHx8-q.js → queryapigate-0.30.0/queryapigate/console_dist/assets/forms-bSOovYin.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/icons-CDledy67.js → queryapigate-0.30.0/queryapigate/console_dist/assets/icons-CeIi0TUG.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/index-C7ega31V.js → queryapigate-0.30.0/queryapigate/console_dist/assets/index-DvCopLMA.js +2 -2
- queryapigate-0.28.0/queryapigate/console_dist/assets/maskRules-D7QV7oGO.js → queryapigate-0.30.0/queryapigate/console_dist/assets/maskRules-9C_A2JIu.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/reach-C1r9_MqV.js → queryapigate-0.30.0/queryapigate/console_dist/assets/reach-DWK2PD5I.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/shared-DLUzWeB2.js → queryapigate-0.30.0/queryapigate/console_dist/assets/shared-rt98BmTW.js +1 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/useMutation-Df0q777p.js → queryapigate-0.30.0/queryapigate/console_dist/assets/useMutation-BcyO6Hjg.js +1 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/index.html +1 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/engine.py +13 -4
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/experimental.py +3 -57
- queryapigate-0.30.0/queryapigate/gunicorn_conf.py +6 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/history.py +25 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/mcp_server.py +5 -3
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/metrics.py +32 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/quotas.py +29 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/secretrefs.py +36 -6
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sso.py +4 -2
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/v1.py +8 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/v1_spec.py +16 -3
- {queryapigate-0.28.0 → queryapigate-0.30.0/queryapigate.egg-info}/PKG-INFO +7 -5
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/SOURCES.txt +37 -33
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/requires.txt +4 -3
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_agent_writes.py +3 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_configcode.py +85 -0
- queryapigate-0.30.0/tests/test_data_protection.py +191 -0
- queryapigate-0.30.0/tests/test_dependency_floors.py +67 -0
- queryapigate-0.30.0/tests/test_docs_site.py +36 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_experimental.py +17 -1
- queryapigate-0.30.0/tests/test_fips.py +90 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_helm.py +29 -4
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp_oauth.py +45 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_observability.py +39 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_publishing.py +2 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_secretrefs.py +70 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_settings.py +1 -1
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sso.py +1 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_upgrades.py +2 -0
- queryapigate-0.28.0/queryapigate/console_dist/assets/AgentsPage-toV9AXCc.js +0 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/ApprovalsPage-eyGXo0ky.js +0 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/Experimental-BD3aGuwW.js +0 -1
- queryapigate-0.28.0/queryapigate/console_dist/assets/SettingsPage-Bi6cE77i.js +0 -3
- queryapigate-0.28.0/queryapigate/console_dist/assets/forms-qgM6Rab7.js +0 -1
- queryapigate-0.28.0/tests/test_dependency_floors.py +0 -41
- {queryapigate-0.28.0 → queryapigate-0.30.0}/LICENSE +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/__main__.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/admins.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/agentwrites.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/alerts.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/apikeys.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/auditchain.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/auditstream.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/aws.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/broadcast.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/bundle.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cache.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/collection_admin.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/configcode.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/assets/index-BLd0t6vZ.css +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/assets/state-O13cl7cO.js +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/console_dist/favicon.svg +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/cors.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/databases.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/db.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/definitions.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/deprecations.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/descriptions.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/destinations.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/duckfiles.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/errors.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/events.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/examples.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/exporting.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/exports.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/formats.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/governance.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/instances.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/jwtauth.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/lib/h2-2.2.224.jar +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/limits.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/logging_setup.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/masking.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/mongotools.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/oidc.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/openapi.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/params.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/parquet.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/pool.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/postman.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/ratelimit.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/rediscache.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/rekey.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/runners.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/schema.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/secheaders.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/__init__.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/access.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/administrators.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/audit.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/collections.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/connections.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/descriptions.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/destinations.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/exports.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/mcp.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/services/queries.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sqlflow.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/sqltools.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/store.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/tableguard.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/tracing.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/usage.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate/vault.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/dependency_links.txt +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/entry_points.txt +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/queryapigate.egg-info/top_level.txt +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/setup.cfg +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_adhoc_history.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_adminroles.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_admins.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_alerts.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_administrators.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_keys.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_api_v1.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_chain.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_log.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_audit_stream.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_aws.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_backup_restore.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_broadcast.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_cli.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collection_bundle.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collection_postman.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_collections.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_connection_encryption.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_console.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_cors_and_rate_limit.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_databases.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_db.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_deprecations.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_descriptions.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_destinations.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_duckdb.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_duckdb_files.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_errors.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_events.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_events_server.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_examples.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_export_security.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_exporting.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_exports.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_history.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_howto.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_instances.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_integration.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_jdbc.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_jwt.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_key_rotation.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_limits.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_masking.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_masking_requests.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mcp_keycloak.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_mongotools.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_multinode.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_oidc.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_params.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_parquet.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_pool.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_query_window.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_queryapigate.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_quotas.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_rekey.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_release_check.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_response_cache.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_roles.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_rolling_upgrade.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_security_headers.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_shared_rate_limit.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sql_guard_fuzz.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sql_guard_writes.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sqlflow.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_sso_keycloak.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_tableguard.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_tracing.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_usage.py +0 -0
- {queryapigate-0.28.0 → queryapigate-0.30.0}/tests/test_vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queryapigate
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.30.0
|
|
4
4
|
Summary: Turn SQL into governed REST APIs over MySQL, PostgreSQL, ClickHouse, SQLite, DuckDB, H2, MongoDB, and Parquet/CSV/JSON files - JSON, CSV, Parquet or Excel out, with scoped keys, an admin Console and MCP for AI agents.
|
|
5
5
|
Author-email: Anantha Raju C <arcswdev@gmail.com>
|
|
6
6
|
License-Expression: FSL-1.1-MIT
|
|
@@ -25,8 +25,9 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
|
25
25
|
Requires-Python: >=3.11
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
License-File: LICENSE
|
|
28
|
-
Requires-Dist: Flask>=
|
|
29
|
-
Requires-Dist:
|
|
28
|
+
Requires-Dist: Flask>=3.0
|
|
29
|
+
Requires-Dist: Werkzeug>=3.0
|
|
30
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
30
31
|
Requires-Dist: openpyxl>=3.1
|
|
31
32
|
Provides-Extra: mysql
|
|
32
33
|
Requires-Dist: mysql-connector-python>=8.0; extra == "mysql"
|
|
@@ -48,7 +49,7 @@ Requires-Dist: cryptography>=41; extra == "encryption"
|
|
|
48
49
|
Provides-Extra: redis
|
|
49
50
|
Requires-Dist: redis>=5; extra == "redis"
|
|
50
51
|
Provides-Extra: mcp
|
|
51
|
-
Requires-Dist: mcp<2,>=1.
|
|
52
|
+
Requires-Dist: mcp<2,>=1.21.1; extra == "mcp"
|
|
52
53
|
Provides-Extra: jwt
|
|
53
54
|
Requires-Dist: PyJWT[crypto]>=2.14; extra == "jwt"
|
|
54
55
|
Provides-Extra: all
|
|
@@ -724,7 +725,8 @@ QueryAPIGate runs whatever SQL it is given against your databases, so it ships l
|
|
|
724
725
|
normal reverse-proxy/TLS-termination setup rather than exposing it directly.
|
|
725
726
|
|
|
726
727
|
See [documentation/THREAT_MODEL.md](documentation/THREAT_MODEL.md) for the reasoning behind these controls -
|
|
727
|
-
what's actually defended against and where it stops -
|
|
728
|
+
what's actually defended against and where it stops - [documentation/DATA_PROTECTION.md](documentation/DATA_PROTECTION.md)
|
|
729
|
+
for what is kept, where and for how long, and [SECURITY.md](SECURITY.md) to report a vulnerability.
|
|
728
730
|
|
|
729
731
|
### Calling the API from a browser
|
|
730
732
|
|
|
@@ -647,7 +647,8 @@ QueryAPIGate runs whatever SQL it is given against your databases, so it ships l
|
|
|
647
647
|
normal reverse-proxy/TLS-termination setup rather than exposing it directly.
|
|
648
648
|
|
|
649
649
|
See [documentation/THREAT_MODEL.md](documentation/THREAT_MODEL.md) for the reasoning behind these controls -
|
|
650
|
-
what's actually defended against and where it stops -
|
|
650
|
+
what's actually defended against and where it stops - [documentation/DATA_PROTECTION.md](documentation/DATA_PROTECTION.md)
|
|
651
|
+
for what is kept, where and for how long, and [SECURITY.md](SECURITY.md) to report a vulnerability.
|
|
651
652
|
|
|
652
653
|
### Calling the API from a browser
|
|
653
654
|
|
|
@@ -26,7 +26,9 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Database",
|
|
27
27
|
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
|
|
28
28
|
]
|
|
29
|
-
|
|
29
|
+
# Werkzeug is imported directly (ProxyFix, ClosingIterator), so it has its own floor. Flask 2.x can't be imported beside
|
|
30
|
+
# Werkzeug 3, which pip installs with it; PyYAML 6.0 has no wheel for Python 3.12 and later, and doesn't build there.
|
|
31
|
+
dependencies = ["Flask>=3.0", "Werkzeug>=3.0", "PyYAML>=6.0.1", "openpyxl>=3.1"]
|
|
30
32
|
|
|
31
33
|
[project.optional-dependencies]
|
|
32
34
|
mysql = ["mysql-connector-python>=8.0"]
|
|
@@ -38,9 +40,9 @@ mongo = ["pymongo>=4.0"]
|
|
|
38
40
|
flow = ["sqlglot>=25.2,<26"] # the Access tab's tables and joins (sqlflow.py) and column masking's tracing (masking.py)
|
|
39
41
|
encryption = ["cryptography>=41"] # only needed when QUERYAPIGATE_SECRET_KEY is set - see store.py's _get_fernet()
|
|
40
42
|
redis = ["redis>=5"] # only needed when QUERYAPIGATE_REDIS_URL is set - see rediscache.py
|
|
41
|
-
mcp = ["mcp>=1.
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
mcp = ["mcp>=1.21.1,<2"] # only needed for `queryapigate mcp` - see mcp_server.py. Structured tool output arrived
|
|
44
|
+
# in 1.10, but nothing before 1.21.1 can be imported beside the pydantic pip installs
|
|
45
|
+
# with it (a private pydantic function they used is gone).
|
|
44
46
|
jwt = ["PyJWT[crypto]>=2.14"] # only needed when QUERYAPIGATE_JWT_SECRET or QUERYAPIGATE_JWT_JWKS_URL is set - see jwtauth.py.
|
|
45
47
|
# 2.14 is the floor because that is where PyJWKClient stops refetching the JWKS for
|
|
46
48
|
# every unknown key id: before it, forged tokens could make the server hammer the provider.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The access log (ADR 0016): one line per request - who asked, for what path, with what result - and never the query
|
|
2
|
+
string's values unless the deployer asks for them.
|
|
3
|
+
|
|
4
|
+
A saved query's parameters can arrive in the URL (`GET /q/orders?customer_email=alice@example.com`), and a web
|
|
5
|
+
server's usual access log writes the whole request line - so every value would be in the log platform for as long as
|
|
6
|
+
it keeps logs. QueryAPIGate's own log, run history and traces all leave values out on purpose; this keeps the access
|
|
7
|
+
log in step. QUERYAPIGATE_ACCESS_LOG_QUERY=1 puts the query string back. The referrer, which can carry another URL's
|
|
8
|
+
query string, is never logged.
|
|
9
|
+
|
|
10
|
+
Used by the image's gunicorn (`gunicorn -c python:queryapigate.gunicorn_conf`) and by `queryapigate serve`.
|
|
11
|
+
"""
|
|
12
|
+
from . import config
|
|
13
|
+
|
|
14
|
+
# gunicorn's atoms: the client, the time, method, path (U: without its query) and protocol, status, size, seconds, the
|
|
15
|
+
# request id QueryAPIGate answered with, and the user agent.
|
|
16
|
+
_WITHOUT_QUERY = '%(h)s - %(t)s "%(m)s %(U)s %(H)s" %(s)s %(b)s %(L)ss rid=%({x-request-id}o)s "%(a)s"'
|
|
17
|
+
_WITH_QUERY = '%(h)s - %(t)s "%(r)s" %(s)s %(b)s %(L)ss rid=%({x-request-id}o)s "%(a)s"'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gunicorn_format():
|
|
21
|
+
"""gunicorn's access_log_format, as QUERYAPIGATE_ACCESS_LOG_QUERY says."""
|
|
22
|
+
return _WITH_QUERY if config.access_log_query() else _WITHOUT_QUERY
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def request_line(method, path, protocol):
|
|
26
|
+
"""A request line as the access log shows it: `path` with its query string removed, unless asked to keep it."""
|
|
27
|
+
if not config.access_log_query():
|
|
28
|
+
path = path.split('?', 1)[0]
|
|
29
|
+
return f'{method} {path} {protocol}'
|
|
@@ -34,12 +34,13 @@ CAPABILITIES = {
|
|
|
34
34
|
'exports.run': 'run an export',
|
|
35
35
|
'destinations.write': 'create, change, test and delete destinations',
|
|
36
36
|
'config.apply': 'see what a configuration file would change, and apply it - any of the above (ADR 0014)',
|
|
37
|
+
'history.forget': "delete a person's runs and usage counts, on request (ADR 0016)",
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
_EVERYONE = {'queries.read', 'connections.read', 'observe', 'self'}
|
|
40
41
|
ROLES = {
|
|
41
42
|
'owner': set(CAPABILITIES),
|
|
42
|
-
'admin': set(CAPABILITIES) - {'admins.read', 'admins.write', 'secrets.rotate', 'config.apply'},
|
|
43
|
+
'admin': set(CAPABILITIES) - {'admins.read', 'admins.write', 'secrets.rotate', 'config.apply', 'history.forget'},
|
|
43
44
|
'developer': _EVERYONE | {'queries.write', 'exports.read', 'exports.run'},
|
|
44
45
|
'auditor': _EVERYONE | {'access.read', 'settings.read', 'exports.read'},
|
|
45
46
|
}
|
|
@@ -92,6 +93,7 @@ OPERATIONS = {
|
|
|
92
93
|
'v1.create_export': 'exports.write', 'v1.update_export': 'exports.write', 'v1.delete_export': 'exports.write',
|
|
93
94
|
'v1.run_export': 'exports.run',
|
|
94
95
|
'v1.config_diff': 'config.apply', 'v1.config_apply': 'config.apply', 'v1.config_managed': 'observe',
|
|
96
|
+
'v1.forget_history': 'history.forget',
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
|
|
@@ -146,6 +146,20 @@ def load_examples_at_startup():
|
|
|
146
146
|
', '.join(f'{name}={secret}' for name, secret in sorted(added['key_secrets'].items())))
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def warn_env_references():
|
|
150
|
+
"""Once at startup: ${VAR} references in use with no QUERYAPIGATE_SECRET_ENV_VARS - so an administrator may
|
|
151
|
+
reference any other variable too, but the few secretrefs.py always refuses (ADR 0017)."""
|
|
152
|
+
from . import destinations, secretrefs
|
|
153
|
+
if config.secret_env_vars():
|
|
154
|
+
return
|
|
155
|
+
used = secretrefs.env_variables([*store.read_connections().values(), *destinations.list_all()])
|
|
156
|
+
if used:
|
|
157
|
+
log.warning('Connections and destinations reference environment variables (%s), and '
|
|
158
|
+
'QUERYAPIGATE_SECRET_ENV_VARS is unset: an administrator could reference any other variable too, '
|
|
159
|
+
"and have the server send it to a host of theirs as a password. Set it to the ones in use.",
|
|
160
|
+
', '.join(used))
|
|
161
|
+
|
|
162
|
+
|
|
149
163
|
def create_app():
|
|
150
164
|
from . import __version__
|
|
151
165
|
config.check_settings()
|
|
@@ -153,6 +167,7 @@ def create_app():
|
|
|
153
167
|
db.init_schema()
|
|
154
168
|
store.refuse_legacy_home()
|
|
155
169
|
experimental.warn_in_use(log, os.environ, store.read_connections())
|
|
170
|
+
warn_env_references()
|
|
156
171
|
instances.register('serve')
|
|
157
172
|
instances.warn_if_unshared(log)
|
|
158
173
|
app = Flask(__name__)
|
|
@@ -321,7 +336,7 @@ def create_app():
|
|
|
321
336
|
deprecations.warn(rule)
|
|
322
337
|
elapsed = time.monotonic() - g.get('request_started', time.monotonic())
|
|
323
338
|
endpoint = request.endpoint or 'unmatched'
|
|
324
|
-
governance.observe(request.method, endpoint, response.status_code, elapsed,
|
|
339
|
+
governance.observe(request.method, endpoint, response.status_code, elapsed, metrics_key_name())
|
|
325
340
|
cache_status = response.headers.get('X-Cache')
|
|
326
341
|
if cache_status == 'HIT':
|
|
327
342
|
metrics.inc_cache_hit()
|
|
@@ -440,6 +455,11 @@ def caller_key_name():
|
|
|
440
455
|
return (permission.name if permission else None) or '-'
|
|
441
456
|
|
|
442
457
|
|
|
458
|
+
def metrics_key_name():
|
|
459
|
+
"""The caller as /metrics counts it (metrics.label_for()): an API key's name, `admin` for any administrator."""
|
|
460
|
+
return metrics.label_for(g.get('permission'))
|
|
461
|
+
|
|
462
|
+
|
|
443
463
|
def get_json_body(required=True):
|
|
444
464
|
data = request.get_json(silent=True)
|
|
445
465
|
if data is None and not required:
|
|
@@ -537,7 +557,7 @@ def stream_sql_response(sql, connection_name, params, timeout, output_format, fi
|
|
|
537
557
|
code='invalid_stream')
|
|
538
558
|
if request.args.get('page') or request.args.get('page_size'):
|
|
539
559
|
raise ApiError('stream=true exports the whole result and does not accept page/page_size', code='invalid_stream')
|
|
540
|
-
key_name =
|
|
560
|
+
key_name = metrics_key_name()
|
|
541
561
|
columns, rows = engine.stream_sql(sql, connection_name, params, timeout, key_name=key_name,
|
|
542
562
|
allowed_tables=allowed_tables, mask=mask)
|
|
543
563
|
# Captured here, not inside _record_stream_history(): that generator's body runs lazily, as the response
|
|
@@ -664,7 +684,7 @@ def execute_sql_endpoint():
|
|
|
664
684
|
try:
|
|
665
685
|
result, elapsed_ms = engine.timed(engine.execute_sql, data['sql'], data['connection_name'], limit, offset,
|
|
666
686
|
params, timeout, allow_writes=g.permission.allow_writes,
|
|
667
|
-
key_name=
|
|
687
|
+
key_name=metrics_key_name(), allowed_write_ops=g.permission.allowed_write_ops,
|
|
668
688
|
database=database, allowed_tables=g.permission.allowed_tables,
|
|
669
689
|
mask=g.permission.mask)
|
|
670
690
|
except ApiError as error:
|
|
@@ -699,7 +719,7 @@ def execute_mongo_endpoint():
|
|
|
699
719
|
result, elapsed_ms = engine.timed(engine.execute_mongo, data['collection'], filter_doc,
|
|
700
720
|
data['connection_name'], limit, offset, params, timeout,
|
|
701
721
|
projection=data.get('projection'), sort=data.get('sort'),
|
|
702
|
-
key_name=
|
|
722
|
+
key_name=metrics_key_name(), mask=g.permission.mask)
|
|
703
723
|
except ApiError as error:
|
|
704
724
|
_record_adhoc_and_broadcast(statement, params, data['connection_name'],
|
|
705
725
|
{**entry, 'status': 'error', 'error': error.message, 'code': error.code})
|
|
@@ -891,7 +911,7 @@ def _run_saved(ref, body, url_params):
|
|
|
891
911
|
entry = run_entry(connection_name)
|
|
892
912
|
try:
|
|
893
913
|
result, elapsed_ms = engine.timed(engine.execute_sql, sql, connection_name, limit, offset, values, timeout,
|
|
894
|
-
allow_writes=effective_allow_writes, key_name=
|
|
914
|
+
allow_writes=effective_allow_writes, key_name=metrics_key_name(),
|
|
895
915
|
allowed_write_ops=g.permission.allowed_write_ops,
|
|
896
916
|
allowed_tables=g.permission.allowed_tables, mask=g.permission.mask)
|
|
897
917
|
except ApiError as error:
|
|
@@ -948,7 +968,7 @@ def _execute_agent_write(request, saved, path, number, connection_name, values,
|
|
|
948
968
|
**({'approved_by': request['decided_by']} if request['decided_by'] else {})}
|
|
949
969
|
try:
|
|
950
970
|
result, affected = engine.execute_agent_write(sql, connection_name, values, saved.get('max_affected_rows') or 1,
|
|
951
|
-
timeout, key_name=
|
|
971
|
+
timeout, key_name=metrics_key_name(),
|
|
952
972
|
allowed_write_ops=g.permission.allowed_write_ops,
|
|
953
973
|
allowed_tables=g.permission.allowed_tables,
|
|
954
974
|
mask=g.permission.mask)
|
|
@@ -999,7 +1019,7 @@ def run_saved_mongo(saved, path, number, ref, connection_name, raw, output_forma
|
|
|
999
1019
|
result, elapsed_ms = engine.timed(engine.execute_mongo, saved['mongo_collection'], filter_doc,
|
|
1000
1020
|
connection_name, limit, offset, timeout=timeout,
|
|
1001
1021
|
projection=saved.get('mongo_projection'), sort=saved.get('mongo_sort'),
|
|
1002
|
-
key_name=
|
|
1022
|
+
key_name=metrics_key_name(), mask=g.permission.mask)
|
|
1003
1023
|
except ApiError as error:
|
|
1004
1024
|
_record_and_broadcast(path, number, connection_name,
|
|
1005
1025
|
{**entry, 'status': 'error', 'error': error.message, 'code': error.code})
|
|
@@ -1142,6 +1162,18 @@ def health():
|
|
|
1142
1162
|
|
|
1143
1163
|
@bp.route('/metrics', methods=['GET'])
|
|
1144
1164
|
def metrics_endpoint():
|
|
1165
|
+
"""Open, unless QUERYAPIGATE_METRICS_TOKEN is set: then for `Authorization: Bearer <that token>` - a scraper's -
|
|
1166
|
+
or an administrator whose role may observe, as the Console's Metrics screen reads it. What the Helm chart sets,
|
|
1167
|
+
since its ingress would otherwise publish the counts and the key, query and connection names in them (ADR 0017's
|
|
1168
|
+
review)."""
|
|
1169
|
+
from . import adminroles
|
|
1170
|
+
role = getattr(g.get('permission'), 'role', None)
|
|
1171
|
+
observer = role in adminroles.ROLES and 'observe' in adminroles.ROLES[role]
|
|
1172
|
+
if not (observer or metrics.authorized(request.headers.get('Authorization'))):
|
|
1173
|
+
response = jsonify(error_body(metrics.UNAUTHORIZED, 401))
|
|
1174
|
+
response.status_code = 401
|
|
1175
|
+
response.headers['WWW-Authenticate'] = 'Bearer realm="metrics"'
|
|
1176
|
+
return response
|
|
1145
1177
|
return Response(metrics.render(current_app.extensions.get('queryapigate_cache')),
|
|
1146
1178
|
mimetype='text/plain; version=0.0.4; charset=utf-8')
|
|
1147
1179
|
|
|
@@ -663,13 +663,37 @@ def _handler():
|
|
|
663
663
|
"""Werkzeug's request handler, without its version banner (`Server: Werkzeug/3.x Python/3.12`, ADR 0010)."""
|
|
664
664
|
from werkzeug.serving import WSGIRequestHandler
|
|
665
665
|
|
|
666
|
+
from . import accesslog
|
|
667
|
+
|
|
666
668
|
class Handler(WSGIRequestHandler):
|
|
667
669
|
def version_string(self):
|
|
668
670
|
return 'QueryAPIGate'
|
|
669
671
|
|
|
672
|
+
def log_request(self, code='-', size='-'):
|
|
673
|
+
"""The access log line without the query string's values (accesslog.py, ADR 0016)."""
|
|
674
|
+
line = accesslog.request_line(self.command, self.path, self.request_version)
|
|
675
|
+
self.log('info', '"%s" %s %s', line, getattr(code, 'value', code), size)
|
|
676
|
+
|
|
670
677
|
return Handler
|
|
671
678
|
|
|
672
679
|
|
|
680
|
+
def _history_forget(args):
|
|
681
|
+
"""`queryapigate history forget CALLER`: delete a person's runs and usage counts (ADR 0016)."""
|
|
682
|
+
from . import history
|
|
683
|
+
try:
|
|
684
|
+
config.check_settings()
|
|
685
|
+
found = history.forget(args.caller, 'cli', dry_run=args.dry_run)
|
|
686
|
+
except (ApiError, ValueError) as error:
|
|
687
|
+
print(f'queryapigate history forget: {getattr(error, "message", error)}', file=sys.stderr)
|
|
688
|
+
return 1
|
|
689
|
+
if args.dry_run:
|
|
690
|
+
print(f"Would delete {found['runs']} run(s) and {found['usage_periods']} usage period(s) of {args.caller}.")
|
|
691
|
+
else:
|
|
692
|
+
print(f"Deleted {found['runs']} run(s) and {found['usage_periods']} usage period(s) of {args.caller}; recorded "
|
|
693
|
+
'in the audit log by its hash, not its name.')
|
|
694
|
+
return 0
|
|
695
|
+
|
|
696
|
+
|
|
673
697
|
def _audit_verify(args):
|
|
674
698
|
"""`queryapigate audit verify [--file PATH]`: walk the audit log's hash chain (ADR 0012) - the store's, or an export
|
|
675
699
|
file's - and say whether it's intact, or where it first breaks. Exit 0 intact, 1 broken, 2 can't be checked
|
|
@@ -894,6 +918,16 @@ def build_parser():
|
|
|
894
918
|
audit_verify.add_argument('--file', help='an export file (QUERYAPIGATE_AUDIT_LOG_EXPORT_FILE) instead of the store')
|
|
895
919
|
audit_verify.set_defaults(func=_audit_verify)
|
|
896
920
|
|
|
921
|
+
history_parser = commands.add_parser('history', help='run history: forget a person (ADR 0016)')
|
|
922
|
+
history_parser.set_defaults(func=lambda a: history_parser.print_help() or 2)
|
|
923
|
+
history_actions = history_parser.add_subparsers(dest='action')
|
|
924
|
+
history_forget = history_actions.add_parser(
|
|
925
|
+
'forget', help="delete every run by CALLER and its usage counts - a person's data, on request; recorded in the "
|
|
926
|
+
'audit log by its hash, not its name')
|
|
927
|
+
history_forget.add_argument('caller', help='a key name, or jwt:<subject> for a signed-in person')
|
|
928
|
+
history_forget.add_argument('--dry-run', action='store_true', help='say how much would go; delete nothing')
|
|
929
|
+
history_forget.set_defaults(func=_history_forget)
|
|
930
|
+
|
|
897
931
|
rekey_parser = commands.add_parser('rekey', help='re-encrypt every stored password under the first key in '
|
|
898
932
|
'QUERYAPIGATE_SECRET_KEY, so the others can be removed (rotating '
|
|
899
933
|
'the key)')
|
|
@@ -64,6 +64,12 @@ def load_examples():
|
|
|
64
64
|
return env_flag('QUERYAPIGATE_LOAD_EXAMPLES')
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def access_log_query():
|
|
68
|
+
"""QUERYAPIGATE_ACCESS_LOG_QUERY: the access log keeps each request's query string - its parameter values. Off by
|
|
69
|
+
default (ADR 0016): the path alone, as QueryAPIGate's own log, run history and traces keep it."""
|
|
70
|
+
return env_flag('QUERYAPIGATE_ACCESS_LOG_QUERY')
|
|
71
|
+
|
|
72
|
+
|
|
67
73
|
def config_file():
|
|
68
74
|
"""QUERYAPIGATE_CONFIG_FILE: a configuration file (ADR 0014) applied at startup, before the server serves - once per
|
|
69
75
|
file: an instance that finds it already applied leaves the store alone. None when unset."""
|
|
@@ -351,6 +357,13 @@ def check_settings():
|
|
|
351
357
|
for folder in os.environ.get('QUERYAPIGATE_SECRET_FILE_DIRS', '').split(','):
|
|
352
358
|
if folder.strip() and not os.path.isabs(folder.strip()):
|
|
353
359
|
raise ValueError(f"QUERYAPIGATE_SECRET_FILE_DIRS: '{folder.strip()}' must be an absolute path")
|
|
360
|
+
if metrics_token() is not None and len(metrics_token()) < METRICS_TOKEN_MIN:
|
|
361
|
+
raise ValueError(f'QUERYAPIGATE_METRICS_TOKEN must be at least {METRICS_TOKEN_MIN} characters - '
|
|
362
|
+
'`openssl rand -hex 32` makes one')
|
|
363
|
+
for name in secret_env_vars():
|
|
364
|
+
if not SECRET_ENV_VAR_RE.fullmatch(name):
|
|
365
|
+
raise ValueError(f"QUERYAPIGATE_SECRET_ENV_VARS: '{name}' must be a variable's name, or a prefix "
|
|
366
|
+
'ending in *')
|
|
354
367
|
# This project was named SQL2API before it was QueryAPIGate, and its settings were SQL2API_*. They are
|
|
355
368
|
# deliberately not read any more - but silently ignoring a leftover SQL2API_API_KEY would start the server
|
|
356
369
|
# with no admin key configured, which means open access. So a leftover old name is a startup error.
|
|
@@ -362,7 +375,7 @@ def check_settings():
|
|
|
362
375
|
raw = os.environ.get('QUERYAPIGATE_LOAD_EXAMPLES', '').strip().lower()
|
|
363
376
|
if raw not in _FLAG_WORDS:
|
|
364
377
|
raise ValueError(f"QUERYAPIGATE_LOAD_EXAMPLES must be yes or no (or 1/0, true/false, on/off), not '{raw}'")
|
|
365
|
-
for name in ('QUERYAPIGATE_CONFIG_PRUNE', 'QUERYAPIGATE_CONFIG_LOCKED'):
|
|
378
|
+
for name in ('QUERYAPIGATE_CONFIG_PRUNE', 'QUERYAPIGATE_CONFIG_LOCKED', 'QUERYAPIGATE_ACCESS_LOG_QUERY'):
|
|
366
379
|
raw = os.environ.get(name, '').strip().lower()
|
|
367
380
|
if raw not in _FLAG_WORDS:
|
|
368
381
|
raise ValueError(f"{name} must be yes or no (or 1/0, true/false, on/off), not '{raw}'")
|
|
@@ -469,6 +482,18 @@ def secret_file_dirs():
|
|
|
469
482
|
return [os.path.realpath(p.strip()) for p in raw.split(',') if p.strip()]
|
|
470
483
|
|
|
471
484
|
|
|
485
|
+
SECRET_ENV_VAR_RE = re.compile(r'[A-Za-z_][A-Za-z0-9_]*\*?')
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def secret_env_vars():
|
|
489
|
+
"""QUERYAPIGATE_SECRET_ENV_VARS: the environment variables `${VAR}` references may read (ADR 0017) - names, or
|
|
490
|
+
prefixes ending in `*` (`WAREHOUSE_*`), comma-separated. Unset, any may be read but those secretrefs.py always
|
|
491
|
+
refuses: the server's own settings, and the cloud, Vault and Kubernetes credentials it may run with. Validated by
|
|
492
|
+
check_settings()."""
|
|
493
|
+
raw = os.environ.get('QUERYAPIGATE_SECRET_ENV_VARS', '')
|
|
494
|
+
return [p.strip() for p in raw.split(',') if p.strip()]
|
|
495
|
+
|
|
496
|
+
|
|
472
497
|
def secret_cache_seconds():
|
|
473
498
|
"""QUERYAPIGATE_SECRET_CACHE_SECONDS: how long a secret resolved from a provider is kept in memory at most (default
|
|
474
499
|
300) - fetched again a little before. Validated by check_settings()."""
|
|
@@ -595,7 +620,8 @@ def _check_vault():
|
|
|
595
620
|
# convention - read at startup, as the environment variable would be.
|
|
596
621
|
SECRET_SETTINGS = ('QUERYAPIGATE_API_KEY', 'QUERYAPIGATE_SECRET_KEY', 'QUERYAPIGATE_DATABASE_URL',
|
|
597
622
|
'QUERYAPIGATE_REDIS_URL', 'QUERYAPIGATE_AUDIT_KEY', 'QUERYAPIGATE_MASK_KEY',
|
|
598
|
-
'QUERYAPIGATE_JWT_SECRET', 'QUERYAPIGATE_SSO_CLIENT_SECRET', 'QUERYAPIGATE_VAULT_SECRET_ID'
|
|
623
|
+
'QUERYAPIGATE_JWT_SECRET', 'QUERYAPIGATE_SSO_CLIENT_SECRET', 'QUERYAPIGATE_VAULT_SECRET_ID',
|
|
624
|
+
'QUERYAPIGATE_METRICS_TOKEN')
|
|
599
625
|
# QUERYAPIGATE_VAULT_TOKEN_FILE isn't one of them: vault.py reads it again at each sign-in, as Vault Agent rotates it.
|
|
600
626
|
_from_files: dict[str, str] = {} # setting -> the file it was read from
|
|
601
627
|
|
|
@@ -626,6 +652,15 @@ def secret_source(name):
|
|
|
626
652
|
return _from_files.get(name)
|
|
627
653
|
|
|
628
654
|
|
|
655
|
+
METRICS_TOKEN_MIN = 16
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def metrics_token():
|
|
659
|
+
"""QUERYAPIGATE_METRICS_TOKEN: the bearer token /metrics needs, or None - then /metrics is open, as it was before
|
|
660
|
+
0.30 (metrics.authorized()). Validated by check_settings()."""
|
|
661
|
+
return os.environ.get('QUERYAPIGATE_METRICS_TOKEN', '').strip() or None
|
|
662
|
+
|
|
663
|
+
|
|
629
664
|
def mask_key():
|
|
630
665
|
"""QUERYAPIGATE_MASK_KEY: the key column masking's `hash` action is keyed with (masking.py), or None - then the
|
|
631
666
|
store makes one once and every instance shares it."""
|
|
@@ -1237,6 +1272,10 @@ def describe_settings():
|
|
|
1237
1272
|
row('Secret files', 'Folders ${file:...} references may read - a connection\'s or destination\'s '
|
|
1238
1273
|
'password from a file the CSI driver, Docker or Vault Agent keeps current (ADR 0015).',
|
|
1239
1274
|
'QUERYAPIGATE_SECRET_FILE_DIRS', ', '.join(secret_file_dirs()) or 'none - ${file:...} refused'),
|
|
1275
|
+
row('Environment secrets', 'Variables ${VAR} references may read. Never the server\'s own settings, nor '
|
|
1276
|
+
'the cloud, Vault and Kubernetes credentials it may run with (AWS_*, AZURE_*, ARM_*, GOOGLE_*, GCP_*, '
|
|
1277
|
+
'VAULT_*, KUBERNETES_*).', 'QUERYAPIGATE_SECRET_ENV_VARS',
|
|
1278
|
+
', '.join(secret_env_vars()) or 'any - set it to the ones your connections use'),
|
|
1240
1279
|
row('Secret cache', 'How long a secret from a provider is kept in memory at most.',
|
|
1241
1280
|
'QUERYAPIGATE_SECRET_CACHE_SECONDS', f'{secret_cache_seconds()} s'),
|
|
1242
1281
|
row('Secret providers', 'How the ones this process has used answered, last.', '',
|
|
@@ -1262,6 +1301,9 @@ def describe_settings():
|
|
|
1262
1301
|
row('Encryption at rest', 'Fernet key used to encrypt connection and destination passwords on disk; '
|
|
1263
1302
|
'several, comma-separated, while rotating it - the first encrypts.',
|
|
1264
1303
|
'QUERYAPIGATE_SECRET_KEY', _encryption_state(), secret=True),
|
|
1304
|
+
row('Metrics token', 'The bearer token /metrics needs. Unset: /metrics is open to whoever can reach it.',
|
|
1305
|
+
'QUERYAPIGATE_METRICS_TOKEN', 'configured' if metrics_token() else 'not set - /metrics is open',
|
|
1306
|
+
secret=True),
|
|
1265
1307
|
row('Masking hash key', "Key for column masking's hash action. Unset: one made once in the store.",
|
|
1266
1308
|
'QUERYAPIGATE_MASK_KEY', 'configured' if mask_key() else 'from the store', secret=True),
|
|
1267
1309
|
row('Export folders', 'Local folders a destination may write under. Unset: object storage only.',
|
|
@@ -1359,8 +1401,12 @@ def describe_settings():
|
|
|
1359
1401
|
row('Ad-hoc history limit', 'Ad-hoc SQL runs kept in all, unless a retention period is set.',
|
|
1360
1402
|
'QUERYAPIGATE_HISTORY_ADHOC_LIMIT', f'{history_adhoc_limit()} runs'),
|
|
1361
1403
|
row('Ad-hoc SQL kept', 'What an ad-hoc run\'s history keeps of its SQL: the text (first '
|
|
1362
|
-
f'{HISTORY_ADHOC_SQL_MAX} characters), a hash, or none
|
|
1404
|
+
f'{HISTORY_ADHOC_SQL_MAX} characters), a hash, or none - and with hash or none, the log shows every '
|
|
1405
|
+
'statement by its hash too. Parameter values are never kept.',
|
|
1363
1406
|
'QUERYAPIGATE_HISTORY_ADHOC_SQL', history_adhoc_sql()),
|
|
1407
|
+
row('Access log query strings', 'The access log keeps each request\'s query string - parameter values '
|
|
1408
|
+
'sent in the URL. Off: the path alone (ADR 0016).', 'QUERYAPIGATE_ACCESS_LOG_QUERY',
|
|
1409
|
+
on_off(access_log_query())),
|
|
1364
1410
|
row('History retention', 'Keep every run for this many days instead of a per-version count. Best '
|
|
1365
1411
|
'with a PostgreSQL metadata store.', 'QUERYAPIGATE_HISTORY_RETENTION_DAYS',
|
|
1366
1412
|
'per-version limit' if history_retention_days() is None else f'{history_retention_days()} days'),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-
|
|
2
|
-
import{$ as e,C as t,D as n,F as r,H as i,I as a,L as o,Q as s,V as c,Z as l,h as u,o as d,s as f,u as p,z as m}from"./index-
|
|
3
|
-
`)),staleTime:1/0});if(d.isPending)return(0,w.jsx)(r,{});if(!f||!p)return(0,w.jsx)(`div`,{className:`hint`,children:`Could not load the SQL.`});let y=_.data?.formatted||h,b=f.created_at,x=f.last_used_at;return(0,w.jsxs)(`div`,{className:`form`,children:[p.description?(0,w.jsx)(`p`,{className:`d-desc`,children:p.description}):null,(0,w.jsxs)(`dl`,{className:`meta`,children:[(0,w.jsx)(T,{term:`connection`,children:p.connection_name||`—`}),(0,w.jsx)(T,{term:`database`,children:s?(0,w.jsx)(`span`,{className:`tag`,children:s}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(T,{term:`collection`,children:f.collection||`—`}),(0,w.jsx)(T,{term:`status`,children:p.status||`—`}),(0,w.jsx)(T,{term:`author`,children:p.author||`—`}),(0,w.jsx)(T,{term:`created`,children:(0,w.jsx)(g,{value:b})}),(0,w.jsx)(T,{term:`last modified`,children:(0,w.jsx)(g,{value:p.last_modified_at||p.created_at})}),(0,w.jsx)(T,{term:`last used`,children:(0,w.jsx)(g,{value:x,fallback:`never`})}),p.tags.length?(0,w.jsx)(T,{term:`tags`,children:p.tags.join(`, `)}):null]}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`SQL · v${p.version}${f.versions.length>1?` (latest)`:``}`}),(0,w.jsx)(`div`,{children:_.isFetching?(0,w.jsx)(r,{text:`Loading SQL…`}):(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(S,{sql:y}),(0,w.jsx)(`div`,{style:{marginTop:6},children:(0,w.jsx)(`button`,{type:`button`,className:`btn sm ghost`,onClick:()=>a(y,u),children:`Copy SQL`})})]})}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`Versions`}),(0,w.jsx)(`div`,{className:`panel`,style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`grid qi-versions`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Version`,`Created`,`Last modified`,`Runs`].map(e=>(0,w.jsx)(`th`,{children:e},e))})}),(0,w.jsx)(`tbody`,{children:[...f.versions].reverse().map(e=>(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`mono`,children:`v`+e.version}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.last_modified_at})}),(0,w.jsx)(`td`,{className:`mono`,style:{textAlign:`right`},children:String(e.run_count??0)})]},e.version))})]})}),(0,w.jsxs)(`div`,{className:`form-actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn`,onClick:l,children:`Close`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{l(),c(`/queries/${encodeURIComponent(e)}`)},children:`Open in API Repository`})]})]})}var D=(0,C.lazy)(()=>c(()=>import(`./forms-BlZHx8-q.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6]))),O={query:`Q`,collection:`C`,connection:`W`},k=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],A=[`database`,`created`,`modified`,`lastUsed`,`version`],j=()=>new Date().toISOString().slice(0,10);function M(){let e=m(),{openDrawer:t}=o(),n=d(),i=f(),a=u(),s=p(),[c,l]=(0,C.useState)(x),g=e=>{let t={...c,...e};_(t),l(t)},v=s.data??[],S=e=>v.find(t=>t.name===e)?.db??null,T=y(c.conn||void 0),O=T.data?.tables??[],k=b(c.conn||void 0,S(c.conn),c.table&&!c.tableMatches?[c.table]:[]),A=c.table?c.tableMatches??(k.data?k.data[c.table]??[]:null):null,j=n.data??[],M=[...new Set(j.map(e=>S(e.connection_name)).filter(e=>!!e))].sort(),P=i.data??{},F=a.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;c.conn?T.isPending?R=(0,w.jsx)(`option`,{value:``,children:`Loading tables…`}):T.isError?R=(0,w.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(`option`,{value:``,children:`All tables (${O.length})`}),O.map(e=>(0,w.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,w.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>h(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,w.jsx)(r,{});else if(!j.length)V=(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,w.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=c.q.trim().toLowerCase(),i=[...j].sort((e,t)=>(e.collection||``)+e.name<(t.collection||``)+t.name?-1:1),a=e=>e.toLowerCase().includes(n),o=n?i.filter(e=>a(e.name)||e.collection&&a(e.collection)):i,s=n?I.filter(a):I,l=n?L.filter(a):L,u=o.length||s.length||l.length,d=o.length?o:u?i:[],f=s.length?s:u?I:[],p=l.length?l:u?L:[];if(c.conn&&(d=d.filter(e=>e.connection_name===c.conn)),c.table&&A===null)V=(0,w.jsx)(r,{text:`Checking which queries reference ${c.table}…`});else if(A&&(d=d.filter(e=>A.includes(e.name))),d.length){let n=d.map(e=>({q:e,dbType:S(e.connection_name),reach:f.filter(t=>B(P[t],e).length).length}));c.db&&(n=n.filter(e=>e.dbType===c.db)),c.reach===`reachable`?n=n.filter(e=>e.reach>0):c.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=c.sort;if(r.field){let e=e=>{switch(r.field){case`query`:return e.q.name.toLowerCase();case`database`:return(e.dbType||``).toLowerCase();case`created`:return e.q.created_at||``;case`modified`:return e.q.updated_at||``;case`lastUsed`:return e.q.last_used_at||``;case`version`:return e.q.latest_version;case`connection`:return(e.q.connection_name||``).toLowerCase();case`reach`:return e.reach;case`col`:{let t=r.colType===`key`?P[r.colName]:F[r.colName];return t&&B(t,e.q).length?1:0}default:return``}};n=[...n].sort((t,n)=>{let i=e(t),a=e(n);return i<a?-r.dir:i>a?r.dir:0})}V=n.length?(0,w.jsx)(N,{rows:n,keyCols:f,roleCols:p,keyMap:P,roleMap:F,st:c,onSort:e=>g({sort:e}),onOpen:t=>e(`/queries/${encodeURIComponent(t)}`),onInfo:e=>t({title:e.q.name,kicker:`${e.q.collection||`uncollected`} · ${e.q.version_count}${e.q.version_count===1?` version`:` versions`}`,content:(0,w.jsx)(E,{name:e.q.name,dbType:e.dbType})})}):(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:c.table?`“${c.table}” on ${c.conn} is not exposed by any saved query yet.`:c.conn?`No saved query uses “${c.conn}” yet.`:`Nothing matches these filters.`;V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`span`,{children:i}),!n&&c.conn?(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=S(c.conn)===`mongo`,i=c.table?n?JSON.stringify({collection:c.table,filter:{}},null,2):`SELECT * FROM `+c.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,w.jsx)(C.Suspense,{fallback:(0,w.jsx)(r,{}),children:(0,w.jsx)(D,{starter:{connection:c.conn,sql:i}})})})},children:`New API`+(c.table?` on `+c.table:``)}):null]})}}return(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Access map`}),(0,w.jsxs)(`span`,{className:`sub`,children:[`Which API keys can reach which saved queries - the whole point of collections and connection grants, in one place.`,` `,(0,w.jsxs)(`span`,{className:`legend`,children:[(0,w.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,w.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,w.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,w.jsx)(`span`,{className:`spacer`}),(0,w.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:v.some(e=>e.name===c.conn)?c.conn:``,onChange:e=>g({conn:e.target.value,table:``,tableMatches:null}),children:[(0,w.jsx)(`option`,{value:``,children:`All connections`}),[...v].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,w.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,w.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:O.some(e=>e.name===c.table)?c.table:``,onChange:e=>g({table:e.target.value,tableMatches:null}),children:R}),(0,w.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:M.includes(c.db)?c.db:``,onChange:e=>g({db:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`All databases`}),M.map(e=>(0,w.jsx)(`option`,{value:e,children:e},e))]}),(0,w.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:c.reach,onChange:e=>g({reach:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`Any reach`}),(0,w.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,w.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,w.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:c.q,onChange:e=>g({q:e.target.value})})]}),(0,w.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function N({rows:e,keyCols:t,roleCols:n,keyMap:r,roleMap:i,st:a,onSort:o,onOpen:s,onInfo:c}){let l=a.sort,u=(e,t=null,n=null)=>l.field===e&&l.colType===t&&l.colName===n,d=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,f=(e,t=null,n=null)=>{let r=u(e,t,n);o(r&&l.dir===1?{field:e,colType:t,colName:n,dir:-1}:r?{field:null,colType:null,colName:null,dir:1}:{field:e,colType:t,colName:n,dir:1})},p=(e,t,n)=>(0,w.jsx)(`th`,{className:t,onClick:()=>f(n),children:e+d(n)},n),m=(e,t,n,r)=>(0,w.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>f(`col`,r,e),children:[(0,w.jsxs)(`span`,{className:`amap-key-name`,children:[e,d(`col`,r,e),n?(0,w.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,w.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,w.jsx)(`span`,{className:`tag`,title:t.mask.map(e=>`${e.column}: ${e.action}`).join(`, `),children:`masked`}):null]})]},r+`:`+e),_=(e,t,n,r)=>{let i=h(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,w.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>O[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,w.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,w.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=j();return(0,w.jsx)(`div`,{style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`amap`,children:[(0,w.jsxs)(`thead`,{children:[(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`th`,{colSpan:3+k.length}),t.length?(0,w.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,w.jsx)(`th`,{colSpan:n.length,className:`amap-group role`,title:`What a key created from each role would reach - a role grants nothing on its own`,children:`ROLES`}):null]}),(0,w.jsxs)(`tr`,{children:[p(`Query`,`amap-query amap-sortable`,`query`),A.map((e,t)=>p(k[t],`amap-sortable`,e)),p(`Connection`,`amap-sortable`,`connection`),p(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return m(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>m(e,i[e],``,`role`))]})]}),(0,w.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,w.jsxs)(`tr`,{children:[(0,w.jsxs)(`td`,{className:`amap-query`,children:[(0,w.jsxs)(`div`,{className:`amap-q-row`,children:[(0,w.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,w.jsx)(`button`,{type:`button`,className:`amap-info`,title:`Details for ${a.name}`,"aria-label":`Details for ${a.name}`,onClick:()=>c(e),children:`i`})]}),a.collection?(0,w.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,w.jsx)(`td`,{children:e.dbType?(0,w.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.updated_at})}),(0,w.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,w.jsx)(g,{value:a.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,w.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,w.jsx)(`td`,{className:`num`,title:`Reachable by an actual API key (roles alone reach nothing)`,children:e.reach?String(e.reach):`—`}),t.map(e=>_(r[e],a,!1,`k:`+e)),n.map(e=>_(i[e],a,!0,`r:`+e))]},a.name)})}),(0,w.jsx)(`tfoot`,{children:(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),k.map(e=>(0,w.jsx)(`td`,{},e)),(0,w.jsx)(`td`,{}),(0,w.jsx)(`td`,{}),t.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{M as AccessMapPage};
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/forms-bSOovYin.js","assets/index-DvCopLMA.js","assets/index-BLd0t6vZ.css","assets/SchemaBrowser-DQ0mi4Vm.js","assets/useMutation-BcyO6Hjg.js","assets/reach-DWK2PD5I.js","assets/CodeBox-DuaKuoNl.js"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{$ as e,C as t,D as n,F as r,H as i,I as a,L as o,Q as s,V as c,Z as l,h as u,o as d,s as f,u as p,z as m}from"./index-DvCopLMA.js";import{a as h}from"./reach-DWK2PD5I.js";import{t as g}from"./Time-KeJDv2te.js";import{f as _,i as v,n as y,r as b,u as x}from"./SchemaBrowser-DQ0mi4Vm.js";import{t as S}from"./CodeBox-DuaKuoNl.js";var C=e(s(),1),w=l();function T({term:e,children:t}){return(0,w.jsxs)(`div`,{children:[(0,w.jsx)(`dt`,{children:e}),(0,w.jsx)(`dd`,{title:typeof t==`string`?t:void 0,children:t})]})}function E({name:e,dbType:s}){let c=m(),{closeDrawer:l,toast:u}=o(),d=i({queryKey:[`queries`,`detail`,e,`info`],queryFn:async()=>n(await t.GET(`/api/v1/queries/{name}`,{params:{path:{name:e}}})),retry:!1}),f=d.data,p=f?.versions[f.versions.length-1],h=p?.query_type===`mongo`?JSON.stringify(p.mongo??{},null,2):p?.sql??``,_=i({queryKey:[`queries`,`flow`,e,p?.version],queryFn:()=>v(e,p.version),enabled:!(!p||p.query_type!==`sql`||h.includes(`
|
|
3
|
+
`)),staleTime:1/0});if(d.isPending)return(0,w.jsx)(r,{});if(!f||!p)return(0,w.jsx)(`div`,{className:`hint`,children:`Could not load the SQL.`});let y=_.data?.formatted||h,b=f.created_at,x=f.last_used_at;return(0,w.jsxs)(`div`,{className:`form`,children:[p.description?(0,w.jsx)(`p`,{className:`d-desc`,children:p.description}):null,(0,w.jsxs)(`dl`,{className:`meta`,children:[(0,w.jsx)(T,{term:`connection`,children:p.connection_name||`—`}),(0,w.jsx)(T,{term:`database`,children:s?(0,w.jsx)(`span`,{className:`tag`,children:s}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(T,{term:`collection`,children:f.collection||`—`}),(0,w.jsx)(T,{term:`status`,children:p.status||`—`}),(0,w.jsx)(T,{term:`author`,children:p.author||`—`}),(0,w.jsx)(T,{term:`created`,children:(0,w.jsx)(g,{value:b})}),(0,w.jsx)(T,{term:`last modified`,children:(0,w.jsx)(g,{value:p.last_modified_at||p.created_at})}),(0,w.jsx)(T,{term:`last used`,children:(0,w.jsx)(g,{value:x,fallback:`never`})}),p.tags.length?(0,w.jsx)(T,{term:`tags`,children:p.tags.join(`, `)}):null]}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`SQL · v${p.version}${f.versions.length>1?` (latest)`:``}`}),(0,w.jsx)(`div`,{children:_.isFetching?(0,w.jsx)(r,{text:`Loading SQL…`}):(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(S,{sql:y}),(0,w.jsx)(`div`,{style:{marginTop:6},children:(0,w.jsx)(`button`,{type:`button`,className:`btn sm ghost`,onClick:()=>a(y,u),children:`Copy SQL`})})]})}),(0,w.jsx)(`p`,{className:`sub-h`,style:{marginTop:6},children:`Versions`}),(0,w.jsx)(`div`,{className:`panel`,style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`grid qi-versions`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Version`,`Created`,`Last modified`,`Runs`].map(e=>(0,w.jsx)(`th`,{children:e},e))})}),(0,w.jsx)(`tbody`,{children:[...f.versions].reverse().map(e=>(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`mono`,children:`v`+e.version}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:e.last_modified_at})}),(0,w.jsx)(`td`,{className:`mono`,style:{textAlign:`right`},children:String(e.run_count??0)})]},e.version))})]})}),(0,w.jsxs)(`div`,{className:`form-actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn`,onClick:l,children:`Close`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{l(),c(`/queries/${encodeURIComponent(e)}`)},children:`Open in API Repository`})]})]})}var D=(0,C.lazy)(()=>c(()=>import(`./forms-bSOovYin.js`).then(e=>({default:e.QueryForm})),__vite__mapDeps([0,1,2,3,4,5,6]))),O={query:`Q`,collection:`C`,connection:`W`},k=[`Database`,`Created`,`Last modified`,`Last used`,`Version`],A=[`database`,`created`,`modified`,`lastUsed`,`version`],j=()=>new Date().toISOString().slice(0,10);function M(){let e=m(),{openDrawer:t}=o(),n=d(),i=f(),a=u(),s=p(),[c,l]=(0,C.useState)(x),g=e=>{let t={...c,...e};_(t),l(t)},v=s.data??[],S=e=>v.find(t=>t.name===e)?.db??null,T=y(c.conn||void 0),O=T.data?.tables??[],k=b(c.conn||void 0,S(c.conn),c.table&&!c.tableMatches?[c.table]:[]),A=c.table?c.tableMatches??(k.data?k.data[c.table]??[]:null):null,j=n.data??[],M=[...new Set(j.map(e=>S(e.connection_name)).filter(e=>!!e))].sort(),P=i.data??{},F=a.data??{},I=Object.keys(P).sort(),L=Object.keys(F).sort(),R,z=!0;c.conn?T.isPending?R=(0,w.jsx)(`option`,{value:``,children:`Loading tables…`}):T.isError?R=(0,w.jsx)(`option`,{value:``,children:`Schema unavailable`}):(z=!1,R=(0,w.jsxs)(w.Fragment,{children:[(0,w.jsx)(`option`,{value:``,children:`All tables (${O.length})`}),O.map(e=>(0,w.jsx)(`option`,{value:e.name,children:`${e.name} · ${e.columns.length}${e.columns.length===1?` col`:` cols`}`},e.name))]})):R=(0,w.jsx)(`option`,{value:``,children:`Select a connection first`});let B=(e,t)=>h(e,t.name,t.collection,t.connection_name),V;if(n.isPending)V=(0,w.jsx)(r,{});else if(!j.length)V=(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`strong`,{children:`No saved queries yet`})});else if(!I.length&&!L.length)V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No scoped API keys or roles yet`}),(0,w.jsx)(`span`,{children:`Every query is reachable by the admin key only until a scoped key is created.`})]});else{let n=c.q.trim().toLowerCase(),i=[...j].sort((e,t)=>(e.collection||``)+e.name<(t.collection||``)+t.name?-1:1),a=e=>e.toLowerCase().includes(n),o=n?i.filter(e=>a(e.name)||e.collection&&a(e.collection)):i,s=n?I.filter(a):I,l=n?L.filter(a):L,u=o.length||s.length||l.length,d=o.length?o:u?i:[],f=s.length?s:u?I:[],p=l.length?l:u?L:[];if(c.conn&&(d=d.filter(e=>e.connection_name===c.conn)),c.table&&A===null)V=(0,w.jsx)(r,{text:`Checking which queries reference ${c.table}…`});else if(A&&(d=d.filter(e=>A.includes(e.name))),d.length){let n=d.map(e=>({q:e,dbType:S(e.connection_name),reach:f.filter(t=>B(P[t],e).length).length}));c.db&&(n=n.filter(e=>e.dbType===c.db)),c.reach===`reachable`?n=n.filter(e=>e.reach>0):c.reach===`unreachable`&&(n=n.filter(e=>e.reach===0));let r=c.sort;if(r.field){let e=e=>{switch(r.field){case`query`:return e.q.name.toLowerCase();case`database`:return(e.dbType||``).toLowerCase();case`created`:return e.q.created_at||``;case`modified`:return e.q.updated_at||``;case`lastUsed`:return e.q.last_used_at||``;case`version`:return e.q.latest_version;case`connection`:return(e.q.connection_name||``).toLowerCase();case`reach`:return e.reach;case`col`:{let t=r.colType===`key`?P[r.colName]:F[r.colName];return t&&B(t,e.q).length?1:0}default:return``}};n=[...n].sort((t,n)=>{let i=e(t),a=e(n);return i<a?-r.dir:i>a?r.dir:0})}V=n.length?(0,w.jsx)(N,{rows:n,keyCols:f,roleCols:p,keyMap:P,roleMap:F,st:c,onSort:e=>g({sort:e}),onOpen:t=>e(`/queries/${encodeURIComponent(t)}`),onInfo:e=>t({title:e.q.name,kicker:`${e.q.collection||`uncollected`} · ${e.q.version_count}${e.q.version_count===1?` version`:` versions`}`,content:(0,w.jsx)(E,{name:e.q.name,dbType:e.dbType})})}):(0,w.jsx)(`div`,{className:`empty`,children:(0,w.jsx)(`span`,{children:`No queries match these filters.`})})}else{let i=n?`Nothing matches “${n}”.`:c.table?`“${c.table}” on ${c.conn} is not exposed by any saved query yet.`:c.conn?`No saved query uses “${c.conn}” yet.`:`Nothing matches these filters.`;V=(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`span`,{children:i}),!n&&c.conn?(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:()=>{let n=S(c.conn)===`mongo`,i=c.table?n?JSON.stringify({collection:c.table,filter:{}},null,2):`SELECT * FROM `+c.table:void 0;e(`/queries`),t({title:`New API`,kicker:`POST /api/v1/queries`,content:(0,w.jsx)(C.Suspense,{fallback:(0,w.jsx)(r,{}),children:(0,w.jsx)(D,{starter:{connection:c.conn,sql:i}})})})},children:`New API`+(c.table?` on `+c.table:``)}):null]})}}return(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Access map`}),(0,w.jsxs)(`span`,{className:`sub`,children:[`Which API keys can reach which saved queries - the whole point of collections and connection grants, in one place.`,` `,(0,w.jsxs)(`span`,{className:`legend`,children:[(0,w.jsx)(`span`,{className:`amap-dot`,children:`Q`}),` named query \xA0`,(0,w.jsx)(`span`,{className:`amap-dot`,children:`C`}),` `,`collection \xA0`,(0,w.jsx)(`span`,{className:`amap-dot conn`,children:`W`}),` whole connection`]})]})]}),(0,w.jsx)(`span`,{className:`spacer`}),(0,w.jsxs)(`select`,{id:`accessmap-conn-filter`,"aria-label":`Connection`,value:v.some(e=>e.name===c.conn)?c.conn:``,onChange:e=>g({conn:e.target.value,table:``,tableMatches:null}),children:[(0,w.jsx)(`option`,{value:``,children:`All connections`}),[...v].sort((e,t)=>e.name<t.name?-1:1).map(e=>(0,w.jsx)(`option`,{value:e.name,children:e.name},e.name))]}),(0,w.jsx)(`select`,{id:`accessmap-table-filter`,"aria-label":`Table`,disabled:z,value:O.some(e=>e.name===c.table)?c.table:``,onChange:e=>g({table:e.target.value,tableMatches:null}),children:R}),(0,w.jsxs)(`select`,{id:`accessmap-db-filter`,"aria-label":`Database`,value:M.includes(c.db)?c.db:``,onChange:e=>g({db:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`All databases`}),M.map(e=>(0,w.jsx)(`option`,{value:e,children:e},e))]}),(0,w.jsxs)(`select`,{id:`accessmap-reach-filter`,"aria-label":`Reach`,value:c.reach,onChange:e=>g({reach:e.target.value}),children:[(0,w.jsx)(`option`,{value:``,children:`Any reach`}),(0,w.jsx)(`option`,{value:`reachable`,children:`Reachable by a key`}),(0,w.jsx)(`option`,{value:`unreachable`,children:`Reachable by no key`})]}),(0,w.jsx)(`input`,{id:`accessmap-filter`,className:`search`,type:`search`,placeholder:`Filter by query or key…`,value:c.q,onChange:e=>g({q:e.target.value})})]}),(0,w.jsx)(`div`,{id:`accessmap-body`,className:`panel`,children:V})]})}function N({rows:e,keyCols:t,roleCols:n,keyMap:r,roleMap:i,st:a,onSort:o,onOpen:s,onInfo:c}){let l=a.sort,u=(e,t=null,n=null)=>l.field===e&&l.colType===t&&l.colName===n,d=(e,t=null,n=null)=>u(e,t,n)?l.dir===1?` ▲`:` ▼`:``,f=(e,t=null,n=null)=>{let r=u(e,t,n);o(r&&l.dir===1?{field:e,colType:t,colName:n,dir:-1}:r?{field:null,colType:null,colName:null,dir:1}:{field:e,colType:t,colName:n,dir:1})},p=(e,t,n)=>(0,w.jsx)(`th`,{className:t,onClick:()=>f(n),children:e+d(n)},n),m=(e,t,n,r)=>(0,w.jsxs)(`th`,{className:`amap-key-head amap-sortable`+(n?` revoked`:``),title:n?e+n:e,onClick:()=>f(`col`,r,e),children:[(0,w.jsxs)(`span`,{className:`amap-key-name`,children:[e,d(`col`,r,e),n?(0,w.jsx)(`span`,{className:`dim`,children:n}):null]}),(0,w.jsxs)(`span`,{className:`amap-key-sub`,children:[(t.allow_writes?`Read/write`:`Read-only`)+` · `+(t.rate_limit||`server default`),t.mask?.length?(0,w.jsx)(`span`,{className:`tag`,title:t.mask.map(e=>`${e.column}: ${e.action}`).join(`, `),children:`masked`}):null]})]},r+`:`+e),_=(e,t,n,r)=>{let i=h(e,t.name,t.collection,t.connection_name);if(!i.length)return(0,w.jsx)(`td`,{className:`amap-cell`},r);let a=[...new Set(i.map(e=>O[e.kind]))],o=a.length===1&&a[0]===`W`;return(0,w.jsx)(`td`,{className:`amap-cell`,title:i.map(e=>e.label).join(`, `),children:(0,w.jsx)(`span`,{className:`amap-dot`+(n?` role`:``)+(o?` conn`:``),children:a.join(`·`)})},r)},v=j();return(0,w.jsx)(`div`,{style:{overflow:`auto`},children:(0,w.jsxs)(`table`,{className:`amap`,children:[(0,w.jsxs)(`thead`,{children:[(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`th`,{colSpan:3+k.length}),t.length?(0,w.jsx)(`th`,{colSpan:t.length,className:`amap-group`,children:`API KEYS`}):null,n.length?(0,w.jsx)(`th`,{colSpan:n.length,className:`amap-group role`,title:`What a key created from each role would reach - a role grants nothing on its own`,children:`ROLES`}):null]}),(0,w.jsxs)(`tr`,{children:[p(`Query`,`amap-query amap-sortable`,`query`),A.map((e,t)=>p(k[t],`amap-sortable`,e)),p(`Connection`,`amap-sortable`,`connection`),p(`Reach`,`num amap-sortable`,`reach`),t.map(e=>{let t=r[e],n=!!(t.expires_at&&t.expires_at<v);return m(e,t,t.active?n?` (expired ${t.expires_at})`:``:` (revoked)`,`key`)}),n.map(e=>m(e,i[e],``,`role`))]})]}),(0,w.jsx)(`tbody`,{children:e.map(e=>{let a=e.q;return(0,w.jsxs)(`tr`,{children:[(0,w.jsxs)(`td`,{className:`amap-query`,children:[(0,w.jsxs)(`div`,{className:`amap-q-row`,children:[(0,w.jsx)(`button`,{type:`button`,className:`amap-q-name`,title:`Open ${a.name} in API Repository`,onClick:()=>s(a.name),children:a.name}),(0,w.jsx)(`button`,{type:`button`,className:`amap-info`,title:`Details for ${a.name}`,"aria-label":`Details for ${a.name}`,onClick:()=>c(e),children:`i`})]}),a.collection?(0,w.jsx)(`span`,{className:`amap-coll`,children:a.collection}):null]}),(0,w.jsx)(`td`,{children:e.dbType?(0,w.jsx)(`span`,{className:`tag`,children:e.dbType}):(0,w.jsx)(`span`,{className:`dim`,children:`—`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.created_at})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(g,{value:a.updated_at})}),(0,w.jsx)(`td`,{className:a.last_used_at?`mono`:`mono dim`,children:(0,w.jsx)(g,{value:a.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:`v`+a.latest_version}),(0,w.jsx)(`td`,{className:`mono dim`,children:a.connection_name||`—`}),(0,w.jsx)(`td`,{className:`num`,title:`Reachable by an actual API key (roles alone reach nothing)`,children:e.reach?String(e.reach):`—`}),t.map(e=>_(r[e],a,!1,`k:`+e)),n.map(e=>_(i[e],a,!0,`r:`+e))]},a.name)})}),(0,w.jsx)(`tfoot`,{children:(0,w.jsxs)(`tr`,{children:[(0,w.jsx)(`td`,{className:`amap-query`,children:`Reaches`}),k.map(e=>(0,w.jsx)(`td`,{},e)),(0,w.jsx)(`td`,{}),(0,w.jsx)(`td`,{}),t.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(r[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t)),n.map(t=>(0,w.jsx)(`td`,{className:`num`,children:`${e.filter(e=>h(i[t],e.q.name,e.q.collection,e.q.connection_name).length).length} / ${e.length}`},t))]})})]})})}export{M as AccessMapPage};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./useMutation-Df0q777p.js";import{$ as t,A as n,C as r,D as i,F as a,H as o,L as s,N as c,O as l,P as u,Q as d,S as f,X as p,Z as m,c as h,d as g,k as _,m as v}from"./index-C7ega31V.js";import{t as y}from"./Time-CZQinVk_.js";import{t as b}from"./forms-qgM6Rab7.js";import{n as x,t as S}from"./icons-CDledy67.js";var C=t(d(),1),w=m(),T=[{value:`owner`,label:`Owner`,text:`everything, including administrators`},{value:`admin`,label:`Admin`,text:`connections, API keys and roles, queries, cache, settings`},{value:`developer`,label:`Developer`,text:`saved queries and SQL; no keys, no connection changes`},{value:`auditor`,label:`Auditor`,text:`read only - audit log, history, configuration; no SQL`}],E=e=>T.find(t=>t.value===e)?.label??e,D=90,O=e=>new Date(Date.now()+e*864e5).toISOString().slice(0,10),k=`Store this now — it cannot be shown again. Paste it into the API key box at the bottom of the sidebar, or send it as the X-API-Key header.`;function A(){let{showError:e}=s();return t=>t instanceof f?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=g(`admins.read`,`other administrators`);return o({queryKey:[`administrators`],queryFn:async()=>(await t.check(),i(await r.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return o({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>i(await r.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=v(),t=h(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=s(),o=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),c;return c=e.isPending?(0,w.jsx)(a,{}):e.isError?(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`Not signed in as an administrator`}),(0,w.jsx)(`span`,{children:`Apply an admin token (qagadm_…) or the shared admin key in the box at the bottom of the sidebar.`})]}):n?r.isPending?(0,w.jsx)(a,{text:`Loading administrators…`}):r.isError?(0,w.jsx)(`div`,{className:`hint`,children:r.error.message}):r.data.length?(0,w.jsx)(P,{items:r.data,self:e.data.via===`token`||e.data.via===`sso`?e.data.name:null}):(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No administrators yet`}),(0,w.jsx)(`span`,{children:`Everyone shares QUERYAPIGATE_API_KEY today, so the audit log can only say “admin”. Give each person their own sign-in, with a role that fits what they do — then the shared key becomes a break-glass key.`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:o,children:`New administrator`})]}):(0,w.jsx)(F,{name:e.data.name??``,role:e.data.role}),(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Administrators`}),(0,w.jsx)(`span`,{className:`sub`,id:`administrators-sub`,children:n?`The people and pipelines that manage this server, each with a role and their own tokens.`:`Your account and your admin tokens.`})]}),(0,w.jsx)(`span`,{className:`spacer`}),n&&(0,w.jsx)(`button`,{id:`new-administrator`,type:`button`,className:`btn primary`,onClick:o,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:c})]})}function P({items:e,self:t}){let{openDrawer:n,toast:i}=s(),a=A(),o=p(),c=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{l(await r.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),i(`Removed `+e),o.invalidateQueries({queryKey:[`administrators`]})}catch(e){a(e)}};return(0,w.jsx)(`div`,{style:{overflowX:`auto`},children:(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Name`,`Role`,`Email`,`Last seen`,`Tokens`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:e.map(e=>(0,w.jsxs)(`tr`,{"data-name":e.name,children:[(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,w.jsx)(`span`,{className:`dot ${e.active?`ok`:`off`}`,title:e.active?`Active`:`Inactive`}),(0,w.jsx)(`span`,{className:`name`,children:e.name}),e.name===t&&(0,w.jsx)(`span`,{className:`tag`,children:`you`}),e.sso?.status===`linked`&&(0,w.jsx)(`span`,{className:`tag`,title:`Signs in through ${e.sso.issuer??`SSO`}; role from their groups`,children:`SSO`}),e.sso?.status===`pending`&&(0,w.jsx)(`span`,{className:`tag`,title:`Linked at their next SSO sign-in, by their verified email`,children:`SSO pending`})]})}),(0,w.jsx)(`td`,{children:E(e.role)}),(0,w.jsx)(`td`,{className:e.email?void 0:`dim`,children:e.email??`—`}),(0,w.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,w.jsx)(y,{value:e.last_seen_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:e.tokens}),(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{className:`actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>n({title:`Admin tokens`,kicker:e.name,wide:!0,content:(0,w.jsx)(R,{name:e.name})}),children:`Tokens`}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon`,title:`Edit ${e.name}`,"aria-label":`Edit`,onClick:()=>n({title:`Edit administrator`,kicker:e.name,content:(0,w.jsx)(I,{name:e.name})}),children:(0,w.jsx)(S,{})}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon danger`,title:`Remove ${e.name}`,"aria-label":`Remove`,onClick:()=>void c(e.name),children:(0,w.jsx)(x,{})})]})})]},e.name))})]})})}function F({name:e,role:t}){return(0,w.jsxs)(`div`,{className:`form`,style:{padding:16},children:[(0,w.jsxs)(`div`,{children:[`Signed in as `,(0,w.jsx)(`strong`,{children:e}),` · `,E(t),` — `,T.find(e=>e.value===t)?.text,`.`]}),(0,w.jsx)(R,{name:e})]})}function I({name:e}){let t=o({queryKey:[`administrators`,`detail`,e],queryFn:async()=>_(await r.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(a,{}):e&&t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):(0,w.jsx)(L,{name:e,existing:t.data?.data,etag:t.data?.etag??null},e??``)}function L({name:t,existing:a,etag:o}){let l=!!t,{closeDrawer:d,openDrawer:f,showError:m,toast:h}=s(),g=A(),_=p(),[v,y]=(0,C.useState)(t??``),[x,S]=(0,C.useState)(a?.role??`developer`),[E,j]=(0,C.useState)(a?.email??``),[M,N]=(0,C.useState)(!a||a.active),P=n(),F=a?.sso?.status===`linked`,[I,L]=(0,C.useState)(a?.sso?.status??`none`),R=e({mutationFn:async()=>{let e={role:x,email:E.trim()||null};if(l)return i(await r.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:t},header:o?{"If-Match":o}:{}},body:{...F?{email:e.email}:e,active:M,...I===(a?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let n=i(await r.POST(`/api/v1/administrators`,{body:{name:v.trim(),...e}}));return i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:n.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:e=>{if(m(``),_.invalidateQueries({queryKey:[`administrators`]}),!e){d(),h(`Updated `+t);return}f({title:`Administrator created`,kicker:e.admin,content:(0,w.jsx)(b,{secret:e.secret,label:`Admin token for ${e.admin} (expires ${e.expires_at})`,hint:k})})},onError:g});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!l&&!v.trim()){m(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(c,{id:`a-name`,label:`Name`,hint:l?void 0:`A person (alice) or a pipeline (ci-deploy). Shown in the audit log.`,children:(0,w.jsx)(`input`,{id:`a-name`,value:v,placeholder:`alice`,autoComplete:`off`,spellCheck:!1,disabled:l,autoFocus:!l,onChange:e=>y(e.target.value)})}),(0,w.jsx)(c,{id:`a-role`,label:`Role`,hint:F?`From their groups at the identity provider, at every sign-in - change it there.`:T.find(e=>e.value===x)?.text,children:(0,w.jsx)(`select`,{id:`a-role`,value:x,disabled:F,onChange:e=>S(e.target.value),children:T.map(e=>(0,w.jsx)(`option`,{value:e.value,children:e.label},e.value))})}),(0,w.jsx)(c,{id:`a-email`,label:`Email`,hint:`Optional.`,children:(0,w.jsx)(`input`,{id:`a-email`,type:`email`,value:E,autoComplete:`off`,onChange:e=>j(e.target.value)})}),l&&(0,w.jsxs)(`label`,{className:`switch`,children:[(0,w.jsx)(`input`,{id:`a-active`,type:`checkbox`,checked:M,onChange:e=>N(e.target.checked)}),`Active`,(0,w.jsx)(`span`,{className:`hint`,children:`— unchecking stops every token of theirs immediately`})]}),l&&P.data?.sso&&(0,w.jsx)(c,{id:`a-sso`,label:`Sign-in through SSO`,hint:I===`linked`?`They sign in through the identity provider. Unlinking ends their sessions; their next SSO sign-in makes a new administrator.`:`Pending: their next SSO sign-in, with a verified email matching the one above, is bound to this account - once.`,children:(0,w.jsxs)(`select`,{id:`a-sso`,value:I,onChange:e=>L(e.target.value),children:[F&&(0,w.jsx)(`option`,{value:`linked`,children:`Linked`}),!F&&(0,w.jsx)(`option`,{value:`pending`,children:`Link at their next sign-in`}),(0,w.jsx)(`option`,{value:`none`,children:F?`Unlink`:`Not through SSO`})]})}),!l&&(0,w.jsxs)(`div`,{className:`hint`,children:[`Their first admin token, valid for `,D,` days, is shown once after you create them.`]}),(0,w.jsx)(u,{onCancel:d,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:l?`Save`:`Create`})})]})}function R({name:t}){let n=M(t),o=p(),{toast:u}=s(),d=A(),[f,m]=(0,C.useState)(``),[h,g]=(0,C.useState)(O(D)),[_,v]=(0,C.useState)(null),x=()=>void o.invalidateQueries({queryKey:[`administrators`]}),S=e({mutationFn:async()=>i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:t}},body:{label:f.trim()||null,expires_at:h||null}})),onSuccess:e=>{v(e),m(``),x()},onError:d}),T=async e=>{if(window.confirm(`Revoke ${e.label?`'${e.label}'`:e.id}? Anything using it stops working now.`))try{l(await r.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:t,token_id:e.id}}})),u(`Revoked `+(e.label??e.id)),x()}catch(e){d(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[n.isPending?(0,w.jsx)(a,{text:`Loading tokens…`}):n.isError?(0,w.jsx)(`div`,{className:`hint`,children:n.error.message}):n.data.length?(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Label`,`Expires`,`Last used`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:n.data.map(e=>(0,w.jsxs)(`tr`,{"data-token":e.id,children:[(0,w.jsx)(`td`,{title:e.id,children:e.label??(0,w.jsx)(`span`,{className:`dim mono`,children:e.id})}),(0,w.jsx)(`td`,{style:{whiteSpace:`nowrap`},children:e.expires_at?(0,w.jsx)(`span`,{style:e.expired?{color:`var(--danger)`}:void 0,children:e.expires_at+(e.expired?` (expired)`:e.expires_at===E?` (today)`:``)}):(0,w.jsx)(`span`,{className:`dim`,children:`never`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(y,{value:e.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{children:(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm danger`,onClick:()=>void T(e),children:`Revoke`})})]},e.id))})]}):(0,w.jsx)(`div`,{className:`hint`,children:`No tokens. Issue one below to sign in with.`}),_?(0,w.jsx)(b,{secret:_.secret,label:`New token${_.label?` '${_.label}'`:``} (${_.expires_at?`expires ${_.expires_at}`:`never expires`})`,hint:k,onDone:()=>v(null)}):(0,w.jsxs)(`form`,{className:`grid2`,noValidate:!0,onSubmit:e=>{e.preventDefault(),S.mutate()},children:[(0,w.jsx)(c,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:f,autoComplete:`off`,onChange:e=>m(e.target.value)})}),(0,w.jsx)(c,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:h,onChange:e=>g(e.target.value)})}),(0,w.jsx)(`div`,{children:(0,w.jsx)(`button`,{type:`submit`,className:`btn`,disabled:S.isPending,children:`Issue token`})})]})]})}export{N as AdministratorsPage};
|
|
1
|
+
import{t as e}from"./useMutation-BcyO6Hjg.js";import{$ as t,A as n,C as r,D as i,F as a,H as o,L as s,N as c,O as l,P as u,Q as d,S as f,X as p,Z as m,c as h,d as g,k as _,m as v}from"./index-DvCopLMA.js";import{t as y}from"./Time-KeJDv2te.js";import{t as b}from"./forms-StJt-xsd.js";import{n as x,t as S}from"./icons-CeIi0TUG.js";var C=t(d(),1),w=m(),T=[{value:`owner`,label:`Owner`,text:`everything, including administrators`},{value:`admin`,label:`Admin`,text:`connections, API keys and roles, queries, cache, settings`},{value:`developer`,label:`Developer`,text:`saved queries and SQL; no keys, no connection changes`},{value:`auditor`,label:`Auditor`,text:`read only - audit log, history, configuration; no SQL`}],E=e=>T.find(t=>t.value===e)?.label??e,D=90,O=e=>new Date(Date.now()+e*864e5).toISOString().slice(0,10),k=`Store this now — it cannot be shown again. Paste it into the API key box at the bottom of the sidebar, or send it as the X-API-Key header.`;function A(){let{showError:e}=s();return t=>t instanceof f?e(t.message,{status:t.status}):e(t.message)}function j(e){let t=g(`admins.read`,`other administrators`);return o({queryKey:[`administrators`],queryFn:async()=>(await t.check(),i(await r.GET(`/api/v1/administrators`)).items),enabled:e,retry:!1})}function M(e){return o({queryKey:[`administrators`,e,`tokens`],queryFn:async()=>i(await r.GET(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:e}}})).items,retry:!1})}function N(){let e=v(),t=h(),n=!!e.data&&t(`admins.read`),r=j(n),{openDrawer:i}=s(),o=()=>i({title:`New administrator`,content:(0,w.jsx)(I,{})}),c;return c=e.isPending?(0,w.jsx)(a,{}):e.isError?(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`Not signed in as an administrator`}),(0,w.jsx)(`span`,{children:`Apply an admin token (qagadm_…) or the shared admin key in the box at the bottom of the sidebar.`})]}):n?r.isPending?(0,w.jsx)(a,{text:`Loading administrators…`}):r.isError?(0,w.jsx)(`div`,{className:`hint`,children:r.error.message}):r.data.length?(0,w.jsx)(P,{items:r.data,self:e.data.via===`token`||e.data.via===`sso`?e.data.name:null}):(0,w.jsxs)(`div`,{className:`empty`,children:[(0,w.jsx)(`strong`,{children:`No administrators yet`}),(0,w.jsx)(`span`,{children:`Everyone shares QUERYAPIGATE_API_KEY today, so the audit log can only say “admin”. Give each person their own sign-in, with a role that fits what they do — then the shared key becomes a break-glass key.`}),(0,w.jsx)(`button`,{type:`button`,className:`btn primary`,onClick:o,children:`New administrator`})]}):(0,w.jsx)(F,{name:e.data.name??``,role:e.data.role}),(0,w.jsxs)(w.Fragment,{children:[(0,w.jsxs)(`div`,{className:`page-head`,children:[(0,w.jsxs)(`div`,{className:`titles`,children:[(0,w.jsx)(`h1`,{children:`Administrators`}),(0,w.jsx)(`span`,{className:`sub`,id:`administrators-sub`,children:n?`The people and pipelines that manage this server, each with a role and their own tokens.`:`Your account and your admin tokens.`})]}),(0,w.jsx)(`span`,{className:`spacer`}),n&&(0,w.jsx)(`button`,{id:`new-administrator`,type:`button`,className:`btn primary`,onClick:o,children:`New administrator`})]}),(0,w.jsx)(`div`,{id:`administrators-table`,className:`panel`,children:c})]})}function P({items:e,self:t}){let{openDrawer:n,toast:i}=s(),a=A(),o=p(),c=async e=>{if(window.confirm(`Remove administrator '${e}'? Every token of theirs stops working immediately.`))try{l(await r.DELETE(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),i(`Removed `+e),o.invalidateQueries({queryKey:[`administrators`]})}catch(e){a(e)}};return(0,w.jsx)(`div`,{style:{overflowX:`auto`},children:(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Name`,`Role`,`Email`,`Last seen`,`Tokens`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:e.map(e=>(0,w.jsxs)(`tr`,{"data-name":e.name,children:[(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8},children:[(0,w.jsx)(`span`,{className:`dot ${e.active?`ok`:`off`}`,title:e.active?`Active`:`Inactive`}),(0,w.jsx)(`span`,{className:`name`,children:e.name}),e.name===t&&(0,w.jsx)(`span`,{className:`tag`,children:`you`}),e.sso?.status===`linked`&&(0,w.jsx)(`span`,{className:`tag`,title:`Signs in through ${e.sso.issuer??`SSO`}; role from their groups`,children:`SSO`}),e.sso?.status===`pending`&&(0,w.jsx)(`span`,{className:`tag`,title:`Linked at their next SSO sign-in, by their verified email`,children:`SSO pending`})]})}),(0,w.jsx)(`td`,{children:E(e.role)}),(0,w.jsx)(`td`,{className:e.email?void 0:`dim`,children:e.email??`—`}),(0,w.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,w.jsx)(y,{value:e.last_seen_at,fallback:`never`})}),(0,w.jsx)(`td`,{className:`mono`,children:e.tokens}),(0,w.jsx)(`td`,{children:(0,w.jsxs)(`div`,{className:`actions`,children:[(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>n({title:`Admin tokens`,kicker:e.name,wide:!0,content:(0,w.jsx)(R,{name:e.name})}),children:`Tokens`}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon`,title:`Edit ${e.name}`,"aria-label":`Edit`,onClick:()=>n({title:`Edit administrator`,kicker:e.name,content:(0,w.jsx)(I,{name:e.name})}),children:(0,w.jsx)(S,{})}),(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm icon danger`,title:`Remove ${e.name}`,"aria-label":`Remove`,onClick:()=>void c(e.name),children:(0,w.jsx)(x,{})})]})})]},e.name))})]})})}function F({name:e,role:t}){return(0,w.jsxs)(`div`,{className:`form`,style:{padding:16},children:[(0,w.jsxs)(`div`,{children:[`Signed in as `,(0,w.jsx)(`strong`,{children:e}),` · `,E(t),` — `,T.find(e=>e.value===t)?.text,`.`]}),(0,w.jsx)(R,{name:e})]})}function I({name:e}){let t=o({queryKey:[`administrators`,`detail`,e],queryFn:async()=>_(await r.GET(`/api/v1/administrators/{name}`,{params:{path:{name:e}}})),enabled:!!e,retry:!1,gcTime:0});return e&&t.isPending?(0,w.jsx)(a,{}):e&&t.isError?(0,w.jsx)(`div`,{className:`hint`,children:t.error.message}):(0,w.jsx)(L,{name:e,existing:t.data?.data,etag:t.data?.etag??null},e??``)}function L({name:t,existing:a,etag:o}){let l=!!t,{closeDrawer:d,openDrawer:f,showError:m,toast:h}=s(),g=A(),_=p(),[v,y]=(0,C.useState)(t??``),[x,S]=(0,C.useState)(a?.role??`developer`),[E,j]=(0,C.useState)(a?.email??``),[M,N]=(0,C.useState)(!a||a.active),P=n(),F=a?.sso?.status===`linked`,[I,L]=(0,C.useState)(a?.sso?.status??`none`),R=e({mutationFn:async()=>{let e={role:x,email:E.trim()||null};if(l)return i(await r.PATCH(`/api/v1/administrators/{name}`,{params:{path:{name:t},header:o?{"If-Match":o}:{}},body:{...F?{email:e.email}:e,active:M,...I===(a?.sso?.status??`none`)?{}:{sso:I===`pending`?`pending`:null}}})),null;let n=i(await r.POST(`/api/v1/administrators`,{body:{name:v.trim(),...e}}));return i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:n.name}},body:{label:`first token`,expires_at:O(D)}}))},onSuccess:e=>{if(m(``),_.invalidateQueries({queryKey:[`administrators`]}),!e){d(),h(`Updated `+t);return}f({title:`Administrator created`,kicker:e.admin,content:(0,w.jsx)(b,{secret:e.secret,label:`Admin token for ${e.admin} (expires ${e.expires_at})`,hint:k})})},onError:g});return(0,w.jsxs)(`form`,{className:`form`,noValidate:!0,onSubmit:e=>{if(e.preventDefault(),!l&&!v.trim()){m(`A name is required.`,{errors:{name:`This field is required`}});return}R.mutate()},children:[(0,w.jsx)(c,{id:`a-name`,label:`Name`,hint:l?void 0:`A person (alice) or a pipeline (ci-deploy). Shown in the audit log.`,children:(0,w.jsx)(`input`,{id:`a-name`,value:v,placeholder:`alice`,autoComplete:`off`,spellCheck:!1,disabled:l,autoFocus:!l,onChange:e=>y(e.target.value)})}),(0,w.jsx)(c,{id:`a-role`,label:`Role`,hint:F?`From their groups at the identity provider, at every sign-in - change it there.`:T.find(e=>e.value===x)?.text,children:(0,w.jsx)(`select`,{id:`a-role`,value:x,disabled:F,onChange:e=>S(e.target.value),children:T.map(e=>(0,w.jsx)(`option`,{value:e.value,children:e.label},e.value))})}),(0,w.jsx)(c,{id:`a-email`,label:`Email`,hint:`Optional.`,children:(0,w.jsx)(`input`,{id:`a-email`,type:`email`,value:E,autoComplete:`off`,onChange:e=>j(e.target.value)})}),l&&(0,w.jsxs)(`label`,{className:`switch`,children:[(0,w.jsx)(`input`,{id:`a-active`,type:`checkbox`,checked:M,onChange:e=>N(e.target.checked)}),`Active`,(0,w.jsx)(`span`,{className:`hint`,children:`— unchecking stops every token of theirs immediately`})]}),l&&P.data?.sso&&(0,w.jsx)(c,{id:`a-sso`,label:`Sign-in through SSO`,hint:I===`linked`?`They sign in through the identity provider. Unlinking ends their sessions; their next SSO sign-in makes a new administrator.`:`Pending: their next SSO sign-in, with a verified email matching the one above, is bound to this account - once.`,children:(0,w.jsxs)(`select`,{id:`a-sso`,value:I,onChange:e=>L(e.target.value),children:[F&&(0,w.jsx)(`option`,{value:`linked`,children:`Linked`}),!F&&(0,w.jsx)(`option`,{value:`pending`,children:`Link at their next sign-in`}),(0,w.jsx)(`option`,{value:`none`,children:F?`Unlink`:`Not through SSO`})]})}),!l&&(0,w.jsxs)(`div`,{className:`hint`,children:[`Their first admin token, valid for `,D,` days, is shown once after you create them.`]}),(0,w.jsx)(u,{onCancel:d,children:(0,w.jsx)(`button`,{type:`submit`,className:`btn primary`,disabled:R.isPending,children:l?`Save`:`Create`})})]})}function R({name:t}){let n=M(t),o=p(),{toast:u}=s(),d=A(),[f,m]=(0,C.useState)(``),[h,g]=(0,C.useState)(O(D)),[_,v]=(0,C.useState)(null),x=()=>void o.invalidateQueries({queryKey:[`administrators`]}),S=e({mutationFn:async()=>i(await r.POST(`/api/v1/administrators/{name}/tokens`,{params:{path:{name:t}},body:{label:f.trim()||null,expires_at:h||null}})),onSuccess:e=>{v(e),m(``),x()},onError:d}),T=async e=>{if(window.confirm(`Revoke ${e.label?`'${e.label}'`:e.id}? Anything using it stops working now.`))try{l(await r.DELETE(`/api/v1/administrators/{name}/tokens/{token_id}`,{params:{path:{name:t,token_id:e.id}}})),u(`Revoked `+(e.label??e.id)),x()}catch(e){d(e)}},E=new Date().toISOString().slice(0,10);return(0,w.jsxs)(`div`,{className:`form`,id:`admin-tokens`,children:[n.isPending?(0,w.jsx)(a,{text:`Loading tokens…`}):n.isError?(0,w.jsx)(`div`,{className:`hint`,children:n.error.message}):n.data.length?(0,w.jsxs)(`table`,{className:`grid`,children:[(0,w.jsx)(`thead`,{children:(0,w.jsx)(`tr`,{children:[`Label`,`Expires`,`Last used`,``].map((e,t)=>(0,w.jsx)(`th`,{children:e},t))})}),(0,w.jsx)(`tbody`,{children:n.data.map(e=>(0,w.jsxs)(`tr`,{"data-token":e.id,children:[(0,w.jsx)(`td`,{title:e.id,children:e.label??(0,w.jsx)(`span`,{className:`dim mono`,children:e.id})}),(0,w.jsx)(`td`,{style:{whiteSpace:`nowrap`},children:e.expires_at?(0,w.jsx)(`span`,{style:e.expired?{color:`var(--danger)`}:void 0,children:e.expires_at+(e.expired?` (expired)`:e.expires_at===E?` (today)`:``)}):(0,w.jsx)(`span`,{className:`dim`,children:`never`})}),(0,w.jsx)(`td`,{className:`mono dim`,children:(0,w.jsx)(y,{value:e.last_used_at,fallback:`never`})}),(0,w.jsx)(`td`,{children:(0,w.jsx)(`button`,{type:`button`,className:`btn ghost sm danger`,onClick:()=>void T(e),children:`Revoke`})})]},e.id))})]}):(0,w.jsx)(`div`,{className:`hint`,children:`No tokens. Issue one below to sign in with.`}),_?(0,w.jsx)(b,{secret:_.secret,label:`New token${_.label?` '${_.label}'`:``} (${_.expires_at?`expires ${_.expires_at}`:`never expires`})`,hint:k,onDone:()=>v(null)}):(0,w.jsxs)(`form`,{className:`grid2`,noValidate:!0,onSubmit:e=>{e.preventDefault(),S.mutate()},children:[(0,w.jsx)(c,{id:`t-label`,label:`New token: label`,hint:`What it's for - laptop, ci.`,children:(0,w.jsx)(`input`,{id:`t-label`,value:f,autoComplete:`off`,onChange:e=>m(e.target.value)})}),(0,w.jsx)(c,{id:`t-expires`,label:`Expires`,hint:`Leave blank for no expiry.`,children:(0,w.jsx)(`input`,{id:`t-expires`,type:`date`,value:h,onChange:e=>g(e.target.value)})}),(0,w.jsx)(`div`,{children:(0,w.jsx)(`button`,{type:`submit`,className:`btn`,disabled:S.isPending,children:`Issue token`})})]})]})}export{N as AdministratorsPage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{$ as e,C as t,D as n,F as r,H as i,Q as a,Z as o}from"./index-DvCopLMA.js";import{t as s}from"./Time-KeJDv2te.js";import{t as c}from"./CodeBox-DuaKuoNl.js";var l=e(a(),1),u=o(),d=[{id:`1h`,label:`Last hour`,hours:1},{id:`24h`,label:`Last 24 hours`,hours:24},{id:`7d`,label:`Last 7 days`,hours:168},{id:`30d`,label:`Last 30 days`,hours:720}],f=e=>e.toLocaleString();function p({refusals:e}){let t=Object.entries(e);return t.length?(0,u.jsx)(`span`,{style:{display:`inline-flex`,flexWrap:`wrap`,gap:4},children:t.map(([e,t])=>(0,u.jsx)(`span`,{className:`tag`,title:`${t} call${t===1?``:`s`} failed with ${e}`,children:`${e} × ${t}`},e))}):(0,u.jsx)(`span`,{className:`dim`,children:`none`})}function m({writes:e}){let t=Object.entries(e??{}).filter(([,e])=>e).map(([e,t])=>`${t} ${e}`);return(0,u.jsx)(`div`,{className:`hint`,children:t.length?t.join(` · `):`None.`})}function h({agent:e}){let t=Object.entries(e.queries);return(0,u.jsxs)(`div`,{className:`agent-detail`,children:[(0,u.jsx)(`p`,{className:`sub-h`,children:`Writes asked for`}),(0,u.jsx)(m,{writes:e.writes}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Saved queries`}),t.length?(0,u.jsx)(`div`,{className:`hint`,children:t.map(([e,t])=>`${e} × ${t}`).join(` · `)}):(0,u.jsx)(`div`,{className:`hint`,children:`None - only ad-hoc SQL.`}),(0,u.jsx)(`p`,{className:`sub-h`,style:{marginTop:12},children:`Most frequent ad-hoc statements`}),e.top_statements.length?e.top_statements.map((e,t)=>(0,u.jsxs)(`div`,{className:`agent-statement`,children:[(0,u.jsx)(`div`,{className:`hint`,children:`${e.count} call${e.count===1?``:`s`} · ${f(e.rows)} rows`+(e.errors?` · ${e.errors} failed`:``)}),e.sql?(0,u.jsx)(c,{sql:e.sql}):(0,u.jsx)(`span`,{className:`mono dim`,children:`sha256 ${e.sql_sha256}`})]},t)):(0,u.jsx)(`div`,{className:`hint`,children:e.adhoc_calls?`Run history keeps no SQL text here (QUERYAPIGATE_HISTORY_ADHOC_SQL is none).`:`No ad-hoc SQL.`})]})}function g(){let[e,a]=(0,l.useState)(`24h`),[o,c]=(0,l.useState)(null),m=d.find(t=>t.id===e).hours,g=i({queryKey:[`usage`,`agents`,e],queryFn:async()=>n(await t.GET(`/api/v1/usage/agents`,{params:{query:{hours:m}}})),retry:!1}),_=g.data?.items??[],v;v=g.isPending?(0,u.jsx)(r,{text:`Loading agents…`}):g.isError?(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`Couldn’t load agent usage`}),(0,u.jsx)(`span`,{children:g.error.message})]}):_.length?(0,u.jsx)(`div`,{style:{overflowX:`auto`},children:(0,u.jsxs)(`table`,{className:`grid`,children:[(0,u.jsx)(`thead`,{children:(0,u.jsx)(`tr`,{children:[`Agent`,`Calls`,`Rows`,`Failed`,`Masked`,`Refused or failed with`,`Last seen`].map(e=>(0,u.jsx)(`th`,{children:e},e))})}),(0,u.jsx)(`tbody`,{children:_.map(e=>(0,u.jsxs)(l.Fragment,{children:[(0,u.jsxs)(`tr`,{children:[(0,u.jsxs)(`td`,{children:[(0,u.jsx)(`button`,{type:`button`,className:`agent-name`,"aria-expanded":o===e.agent,onClick:()=>c(o===e.agent?null:e.agent),children:e.agent}),(0,u.jsx)(`div`,{className:`dim`,style:{fontSize:12},children:e.connections.join(`, `)})]}),(0,u.jsx)(`td`,{className:`num`,children:f(e.calls)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.rows)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.errors)}),(0,u.jsx)(`td`,{className:`num`,children:f(e.masked_calls)}),(0,u.jsx)(`td`,{children:(0,u.jsx)(p,{refusals:e.refusals})}),(0,u.jsx)(`td`,{className:`mono dim`,style:{whiteSpace:`nowrap`},children:(0,u.jsx)(s,{value:e.last_seen,fallback:``})})]}),o===e.agent&&(0,u.jsx)(`tr`,{children:(0,u.jsx)(`td`,{colSpan:7,children:(0,u.jsx)(h,{agent:e})})})]},e.agent))})]})}):(0,u.jsxs)(`div`,{className:`empty`,children:[(0,u.jsx)(`strong`,{children:`No agent called the MCP server in this window`}),(0,u.jsx)(`span`,{children:`Runs over MCP - saved-query tools and execute_sql - appear here, per API key or signed-in user.`})]});let y=[];return g.data&&g.data.sample_rate<1&&y.push(`Run history keeps ${Math.round(g.data.sample_rate*100)}% of successful runs, so successful calls and rows are a sample; failures are all here.`),g.data?.truncated&&y.push(`More runs than one summary reads (100,000) - narrow the window.`),(0,u.jsxs)(u.Fragment,{children:[(0,u.jsxs)(`div`,{className:`page-head`,children:[(0,u.jsxs)(`div`,{className:`titles`,children:[(0,u.jsx)(`h1`,{children:`Agents`}),(0,u.jsx)(`span`,{className:`sub`,children:`What each AI agent did over MCP - per API key or signed-in user, from run history.`})]}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`button`,{type:`button`,className:`btn`,onClick:()=>void g.refetch(),children:`Refresh`})]}),(0,u.jsxs)(`div`,{className:`panel`,style:{overflow:`hidden`},children:[(0,u.jsxs)(`div`,{className:`panel-bar`,children:[(0,u.jsx)(`select`,{"aria-label":`Window`,value:e,onChange:e=>a(e.target.value),children:d.map(e=>(0,u.jsx)(`option`,{value:e.id,children:e.label},e.id))}),(0,u.jsx)(`span`,{className:`spacer`}),(0,u.jsx)(`span`,{className:`panel-count`,children:g.data?`${_.length} agent${_.length===1?``:`s`}`:``})]}),y.map(e=>(0,u.jsx)(`div`,{className:`hint`,style:{padding:`6px 12px`},children:e},e)),v]})]})}export{g as AgentsPage};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{$ as e,B as t,F as n,Q as r,Z as i,_ as a,b as o,x as s,y as c,z as l}from"./index-
|
|
1
|
+
import{$ as e,B as t,F as n,Q as r,Z as i,_ as a,b as o,x as s,y as c,z as l}from"./index-DvCopLMA.js";import{t as u}from"./Time-KeJDv2te.js";import{n as d}from"./state-O13cl7cO.js";var f=e(r(),1),p=i(),m=[{id:`critical`,label:`Critical`},{id:`warning`,label:`Warning`},{id:`info`,label:`For your information`}],h={key:`Open API keys`,query:`Open query`,connection:`Open connections`,settings:`Open settings`,export:`Open exports`,agent:`Open agents`,approvals:`Open approvals`,audit:`Open audit log`};function g(){let e=l();return t=>{t.type===`query`?e(`/queries/${encodeURIComponent(t.name)}`):t.type===`key`?e(`/api-keys`):t.type===`connection`?e(`/connections`):t.type===`export`?e(`/exports`):t.type===`agent`?e(`/agents`):t.type===`approvals`?e(`/approvals`):t.type===`audit`?e(`/audit-log`):(d(t.name),e(`/settings`))}}function _({alert:e,dismissed:t}){let n=g();return(0,p.jsxs)(`div`,{className:`alert-row `+e.severity,"data-alert":e.id,children:[(0,p.jsx)(`span`,{className:`sev `+e.severity,children:e.severity}),(0,p.jsxs)(`div`,{className:`alert-what`,children:[(0,p.jsx)(`strong`,{children:e.title}),(0,p.jsx)(`span`,{className:`alert-detail`,children:e.detail}),e.since&&!e.detail.includes(e.since)&&(0,p.jsxs)(`span`,{className:`alert-since`,children:[e.kind===`key_expiring`?`Expires `:`Since `,(0,p.jsx)(u,{value:e.since})]})]}),(0,p.jsxs)(`div`,{className:`actions`,children:[e.target&&(0,p.jsx)(`button`,{type:`button`,className:`btn sm outlined`,onClick:()=>n(e.target),children:h[e.target.type]??`Open`}),t?(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>o(e.id),children:`Restore`}):(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,title:`Hide it in this browser until it clears`,onClick:()=>c(e.id),children:`Dismiss`})]})]})}function v({alerts:e}){return(0,p.jsx)(p.Fragment,{children:m.map(({id:t,label:n})=>{let r=e.filter(e=>e.severity===t);return r.length?(0,p.jsxs)(`div`,{className:`panel alerts-group`,id:`alerts-`+t,children:[(0,p.jsxs)(`h2`,{children:[n,` `,(0,p.jsx)(`span`,{className:`count`,children:r.length})]}),r.map(e=>(0,p.jsx)(_,{alert:e},e.id))]},t):null})})}function y(){let e=l(),r=t(),i=a.find(e=>e.id===r.tab),{query:o,active:c,dismissed:d}=s(),[m,h]=(0,f.useState)(!1),g=e=>!i||i.kinds.includes(e.kind),y=c.filter(g),b=d.filter(g),x=e=>c.filter(t=>!e||e.includes(t.kind)),S;S=o.isPending?(0,p.jsx)(n,{text:`Checking…`}):o.isError?(0,p.jsx)(`div`,{className:`panel`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`Couldn’t check for alerts`}),(0,p.jsx)(`span`,{children:o.error.message}),(0,p.jsx)(`button`,{type:`button`,className:`btn sm`,onClick:()=>void o.refetch(),children:`Retry`})]})}):(0,p.jsxs)(p.Fragment,{children:[i&&(0,p.jsx)(`p`,{className:`alerts-about`,children:i.about}),y.length?(0,p.jsx)(v,{alerts:y}):(0,p.jsx)(`div`,{className:`panel`,id:`alerts-clear`,children:(0,p.jsxs)(`div`,{className:`empty`,children:[(0,p.jsx)(`strong`,{children:`All clear`}),(0,p.jsx)(`span`,{children:i?`Nothing to report from this check right now.`:`Nothing needs your attention right now.`})]})}),b.length>0&&(0,p.jsxs)(`div`,{className:`alerts-dismissed`,children:[(0,p.jsx)(`button`,{type:`button`,className:`btn ghost sm`,onClick:()=>h(e=>!e),children:`${b.length} dismissed in this browser · ${m?`Hide`:`Show`}`}),m&&(0,p.jsx)(`div`,{className:`panel alerts-group`,id:`alerts-dismissed`,children:b.map(e=>(0,p.jsx)(_,{alert:e,dismissed:!0},e.id))})]})]});let C=(t,n,r)=>(0,p.jsxs)(`button`,{type:`button`,role:`tab`,"aria-selected":(i?.id??`all`)===t,className:`minitab`+((i?.id??`all`)===t?` active`:``),onClick:()=>e(t===`all`?`/alerts`:`/alerts/`+t),children:[n,o.data?(0,p.jsx)(`span`,{className:`n`+(r.length?` on`:``)+(r.some(e=>e.severity===`critical`)?` critical`:``),children:r.length}):null]},t);return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`div`,{className:`page-head`,children:[(0,p.jsxs)(`div`,{className:`titles`,children:[(0,p.jsx)(`h1`,{children:`Alerts`}),(0,p.jsxs)(`span`,{className:`sub`,children:[`What needs attention now, checked every minute. Each clears by itself once its cause does.`,o.data?(0,p.jsxs)(p.Fragment,{children:[` Last checked `,(0,p.jsx)(u,{value:o.data.checked_at}),`.`]}):null]})]}),(0,p.jsx)(`span`,{className:`spacer`}),(0,p.jsx)(`button`,{id:`refresh-alerts`,type:`button`,className:`btn`,onClick:()=>void o.refetch(),children:`Check now`})]}),(0,p.jsxs)(`div`,{className:`minitabs alerts-tabs`,role:`tablist`,"aria-label":`Checks`,children:[C(`all`,`All`,x()),a.map(e=>C(e.id,e.label,x(e.kinds)))]}),S]})}export{y as AlertsPage};
|