datex-studio-cli 0.4.11__tar.gz → 0.4.12__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.
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/PKG-INFO +1 -1
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/pyproject.toml +2 -1
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/__init__.py +1 -1
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/cli.py +24 -1
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/auth.py +34 -19
- datex_studio_cli-0.4.12/src/dxs/core/auth/datex_tenant.py +265 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/msal_client.py +13 -3
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/client.py +40 -68
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/client.py +39 -67
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/.gitignore +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/README.md +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/report-creator.skill +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/scripts/analyze_corpus.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/scripts/generate_rdlx_schema.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/__main__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/api.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/branch.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/config.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/configuration.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/crm.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/datasource.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/devops.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/document.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/endpoint.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/env.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/explore.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/function.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/init.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/marketplace.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/odata.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/organization.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/proxy.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/release_notes.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/repo.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/add_cmds.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/api.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/batch.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/create.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/data.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/dataset.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/edit.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/folder.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/preview.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/report/schema_cmds.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/schema.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/servicepack.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/source.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/studio.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/telemetry.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/commands/user.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/context.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/app_config.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/client.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/endpoints.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/metadata_models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/api/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/decorators.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/auth/token_cache.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/cache.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/flow_generator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/flow_models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/generator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/parsers.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/resolver.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/type_def_parser.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/datasource/validator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/flow_models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/designer/parsers.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/helpers.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/devops/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/dynamics/metadata_cache.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/client.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/edmx_parser.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/footprint/metadata.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/generator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/function/validator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/graph.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/onboarding.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/csv_fmt.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/formatter.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/json_fmt.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/redaction.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/output/yaml_fmt.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/release_notes.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/responses.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/config.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/identity.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/recorder.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/scrubber.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/core/telemetry/sender.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/models/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/py.typed +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/datasource_binding.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/engine.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/field_parser.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/folder.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/manifest.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/models.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/preview.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/preview_arjs.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/schema.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/svg_renderer.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/bill_of_lading.rdlx-json +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/templates/shipping_label.rdlx-json +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/validate_arjs.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/validator.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/report/wrapper.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/click_options.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/clipboard.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/config.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/errors.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/filtering.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/image.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/paths.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/resolvers.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/responses.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/restricted.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/sorting.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/utils/update_check.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/__init__.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/app.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/design.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/proxy_app.py +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/404/index.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/404.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_buildManifest.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/UgyAMtPjH-Pt0RYWOHzIT/_ssgManifest.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/0b24cca5-c1e1c8810348f107.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/255-102f2e5b2e3dc2ef.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/4bd1b696-c023c6e3521b1417.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/538-84e2e111415f1fda.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/697-0e000ab410d9f470.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/871-1acacdb7b4022abf.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/8b8f67fc-6295949a4b5485a4.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/909-c88abba3cf0caec3.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/_not-found/page-ea1be7001c230704.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/design/capture/page-a5e129c2d223432c.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/design/page-21ba66a3dd3b03f0.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/layout-2cc6eac09e476c91.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/app/page-20c358395882cdac.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/framework-de98b93a850cfc71.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/main-0f18f91200dac003.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/main-app-b69998d8941231d8.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/pages/_app-7d307437aca18ad4.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/pages/_error-cb2a52f75f2162e2.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/chunks/webpack-2b297ada5306c17f.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/_next/static/css/360c657de5e04df8.css +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/capture/index.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/capture/index.txt +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/index.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/design/index.txt +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/index.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/index.txt +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/render-cli.js +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/report-preview.html +0 -0
- {datex_studio_cli-0.4.11 → datex_studio_cli-0.4.12}/src/dxs/web/static/report-validate.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datex-studio-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.12
|
|
4
4
|
Summary: CLI for Datex Studio low-code platform, designed for LLM-based AI agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/datex/datex-studio-cli
|
|
6
6
|
Project-URL: Documentation, https://github.com/datex/datex-studio-cli
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datex-studio-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.12"
|
|
8
8
|
description = "CLI for Datex Studio low-code platform, designed for LLM-based AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -152,6 +152,7 @@ python_functions = ["test_*"]
|
|
|
152
152
|
addopts = "-v --tb=short"
|
|
153
153
|
markers = [
|
|
154
154
|
"fidelity: cross-engine SSIM fidelity tests (require agent-browser + ARJS)",
|
|
155
|
+
"internal_commands_gated: exercise the devops/crm visibility gate (no DXS_INTERNAL_COMMANDS override)",
|
|
155
156
|
]
|
|
156
157
|
|
|
157
158
|
[tool.coverage.run]
|
|
@@ -8,6 +8,7 @@ import click
|
|
|
8
8
|
|
|
9
9
|
from dxs import __app_name__, __version__
|
|
10
10
|
from dxs.context import DxsContext, pass_context # re-exported for backward compat
|
|
11
|
+
from dxs.core.auth.datex_tenant import DATEX_INTERNAL_COMMANDS, internal_commands_available
|
|
11
12
|
from dxs.core.output import OutputFormat, format_error
|
|
12
13
|
from dxs.core.output.redaction import (
|
|
13
14
|
AUTHOR_VERBOSE_FIELDS as _AUTHOR_VERBOSE_FIELDS, # noqa: F401 — re-exported
|
|
@@ -77,7 +78,29 @@ CONTEXT_SETTINGS = {
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
class DxsGroup(click.Group):
|
|
80
|
-
"""Custom Group that shows global options in subcommand help.
|
|
81
|
+
"""Custom Group that shows global options in subcommand help.
|
|
82
|
+
|
|
83
|
+
Also gates the Datex-internal command groups (``devops``, ``crm``). The gate
|
|
84
|
+
is applied at lookup time rather than at registration so it reflects the
|
|
85
|
+
identity cache as of *this* invocation — registration happens once at import.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def _is_gated(self, cmd_name: str) -> bool:
|
|
89
|
+
"""Whether ``cmd_name`` is Datex-internal and unavailable to this user."""
|
|
90
|
+
return cmd_name in DATEX_INTERNAL_COMMANDS and not internal_commands_available()
|
|
91
|
+
|
|
92
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
93
|
+
"""List visible subcommands, omitting unavailable Datex-internal groups."""
|
|
94
|
+
return [name for name in super().list_commands(ctx) if not self._is_gated(name)]
|
|
95
|
+
|
|
96
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
97
|
+
"""Resolve a subcommand, refusing unavailable Datex-internal groups."""
|
|
98
|
+
if self._is_gated(cmd_name):
|
|
99
|
+
ctx.fail(
|
|
100
|
+
f"'{cmd_name}' is a Datex-internal command group and requires a "
|
|
101
|
+
"Datex tenant identity. Run 'dxs auth login' with your Datex account."
|
|
102
|
+
)
|
|
103
|
+
return super().get_command(ctx, cmd_name)
|
|
81
104
|
|
|
82
105
|
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
83
106
|
"""Write options to the formatter, including global options."""
|
|
@@ -8,6 +8,7 @@ import click
|
|
|
8
8
|
|
|
9
9
|
from dxs.context import DxsContext, pass_context
|
|
10
10
|
from dxs.core.auth import MSALClient, MultiIdentityTokenCache
|
|
11
|
+
from dxs.core.auth.datex_tenant import claims_are_datex, is_datex_identity
|
|
11
12
|
from dxs.core.auth.decorators import get_access_token
|
|
12
13
|
from dxs.core.auth.token_cache import CachedIdentity, get_identity_override
|
|
13
14
|
from dxs.core.footprint import get_footprint_token
|
|
@@ -78,23 +79,33 @@ def run_standard_device_login(
|
|
|
78
79
|
consent_cb = on_additional_consent_needed or _on_consent
|
|
79
80
|
|
|
80
81
|
ctx.log("Initiating device code authentication...")
|
|
82
|
+
additional_scope_sets: Any
|
|
81
83
|
if tenant_id is not None:
|
|
82
84
|
client = MSALClient.for_tenant(tenant_id, scopes=settings.azure_scopes)
|
|
83
85
|
# External tenants only need Studio API + Footprint; DevOps/CRM are not relevant.
|
|
84
86
|
additional_scope_sets = [[settings.footprint_scope]]
|
|
85
87
|
else:
|
|
86
88
|
client = MSALClient(scopes=settings.azure_scopes)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
|
|
90
|
+
def additional_scope_sets(token_response: dict[str, Any]) -> list[list[str]]:
|
|
91
|
+
"""Choose consent scopes from the claims of whoever just signed in.
|
|
92
|
+
|
|
93
|
+
The home-tenant path is also what customers hit before their own
|
|
94
|
+
tenant is known, so DevOps/CRM consent is requested only for Datex
|
|
95
|
+
staff — a customer must never be prompted for Datex-internal
|
|
96
|
+
resources by name.
|
|
97
|
+
"""
|
|
98
|
+
internal = (
|
|
99
|
+
[
|
|
100
|
+
settings.azure_devops_scopes,
|
|
101
|
+
settings.dynamics_crm_scopes,
|
|
102
|
+
settings.dynamics_crm_admin_scopes,
|
|
103
|
+
settings.data_export_service_scopes,
|
|
104
|
+
]
|
|
105
|
+
if claims_are_datex(token_response.get("id_token_claims") or {})
|
|
106
|
+
else []
|
|
107
|
+
)
|
|
108
|
+
return [scopes for scopes in [*internal, [settings.footprint_scope]] if scopes]
|
|
98
109
|
|
|
99
110
|
result = client.authenticate_device_code_multi_resource(
|
|
100
111
|
primary_scopes=settings.azure_scopes,
|
|
@@ -638,17 +649,21 @@ def status(ctx: DxsContext) -> None:
|
|
|
638
649
|
"name": "Datex Studio API",
|
|
639
650
|
"scopes": settings.azure_scopes,
|
|
640
651
|
},
|
|
641
|
-
"azure_devops": {
|
|
642
|
-
"name": "Azure DevOps API",
|
|
643
|
-
"scopes": settings.azure_devops_scopes,
|
|
644
|
-
},
|
|
645
652
|
}
|
|
646
653
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
654
|
+
# DevOps and CRM are Datex-internal. Probing them with a customer identity
|
|
655
|
+
# only ever yields noise, so omit those rows entirely unless the identity
|
|
656
|
+
# being reported is a Datex one.
|
|
657
|
+
if is_datex_identity(active_identity):
|
|
658
|
+
resources["azure_devops"] = {
|
|
659
|
+
"name": "Azure DevOps API",
|
|
660
|
+
"scopes": settings.azure_devops_scopes,
|
|
651
661
|
}
|
|
662
|
+
if settings.dynamics_crm_scopes:
|
|
663
|
+
resources["dynamics_crm"] = {
|
|
664
|
+
"name": "Dynamics CRM API",
|
|
665
|
+
"scopes": settings.dynamics_crm_scopes,
|
|
666
|
+
}
|
|
652
667
|
|
|
653
668
|
resources["footprint"] = {
|
|
654
669
|
"name": "Footprint API",
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""Datex-tenant (staff-only) identity resolution.
|
|
2
|
+
|
|
3
|
+
Azure DevOps and Dynamics CRM are Datex-internal resources: they live in the
|
|
4
|
+
Datex Entra tenant and only Datex staff can reach them. Two consequences drive
|
|
5
|
+
this module.
|
|
6
|
+
|
|
7
|
+
**Customers must never see them.** A customer authenticates either against their
|
|
8
|
+
own tenant (``external_entra_id``) or, for organizations that do not require
|
|
9
|
+
external auth, as a B2B guest inside the Datex tenant. Neither can use
|
|
10
|
+
``dxs devops`` or ``dxs crm``, so those groups are hidden from ``--help`` and
|
|
11
|
+
their token rows are omitted from ``dxs auth status``.
|
|
12
|
+
|
|
13
|
+
**Staff must keep them across an identity switch.** ``dxs auth switch CAG``
|
|
14
|
+
makes a customer identity active, but the cached Datex identity is still valid;
|
|
15
|
+
DevOps/CRM tokens are therefore minted from *that* identity rather than from
|
|
16
|
+
whichever one happens to be active. The rotated refresh token is written back
|
|
17
|
+
onto the Datex identity so it stays alive while work happens elsewhere.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from dxs.core.auth.msal_client import MSALClient
|
|
25
|
+
from dxs.core.auth.token_cache import (
|
|
26
|
+
CachedIdentity,
|
|
27
|
+
MultiIdentityTokenCache,
|
|
28
|
+
get_identity_override,
|
|
29
|
+
)
|
|
30
|
+
from dxs.utils.config import get_settings
|
|
31
|
+
from dxs.utils.errors import AuthenticationError
|
|
32
|
+
|
|
33
|
+
# Command groups that require a Datex-tenant identity.
|
|
34
|
+
DATEX_INTERNAL_COMMANDS = frozenset({"crm", "devops"})
|
|
35
|
+
|
|
36
|
+
# Entra stamps B2B guest UPNs with this marker (``user_theirdomain.com#EXT#@…``).
|
|
37
|
+
# It is the signal that separates a customer invited into the Datex tenant from
|
|
38
|
+
# an actual Datex staff member, since both carry the Datex ``tid``.
|
|
39
|
+
GUEST_UPN_MARKER = "#EXT#"
|
|
40
|
+
|
|
41
|
+
# Escape hatch for support and CI: force the internal command groups on or off
|
|
42
|
+
# regardless of the cached identities.
|
|
43
|
+
INTERNAL_COMMANDS_ENV = "DXS_INTERNAL_COMMANDS"
|
|
44
|
+
|
|
45
|
+
# Pre-acquired resource tokens injected by an external host (e.g. Sidekick).
|
|
46
|
+
# Their presence means the caller has already arranged internal access.
|
|
47
|
+
INJECTED_RESOURCE_TOKEN_ENV = ("DXS_DEVOPS_TOKEN", "DXS_DYNAMICS_TOKEN")
|
|
48
|
+
|
|
49
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
50
|
+
_FALSY = {"0", "false", "no", "off"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _home_tenant_id() -> str:
|
|
54
|
+
"""The Datex tenant ID (``azure_tenant_id``, the CLI's configured home tenant)."""
|
|
55
|
+
return get_settings().azure_tenant_id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_guest_upn(username: str | None) -> bool:
|
|
59
|
+
"""Whether a username is a B2B guest UPN rather than a native account."""
|
|
60
|
+
return GUEST_UPN_MARKER in (username or "").upper()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_datex_identity(identity: CachedIdentity) -> bool:
|
|
64
|
+
"""Whether ``identity`` is a Datex staff identity.
|
|
65
|
+
|
|
66
|
+
Requires the Datex tenant *and* a native (non-guest) account. The tenant
|
|
67
|
+
check alone is not enough: organizations with ``externalEntraId=false``
|
|
68
|
+
authenticate their users against the Datex tenant as guests, so they would
|
|
69
|
+
otherwise be mistaken for staff.
|
|
70
|
+
"""
|
|
71
|
+
if identity.external_entra_id:
|
|
72
|
+
return False
|
|
73
|
+
if not identity.tenant_id or identity.tenant_id.lower() != _home_tenant_id().lower():
|
|
74
|
+
return False
|
|
75
|
+
return not _is_guest_upn(identity.account_username)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def resolve_datex_identity(
|
|
79
|
+
cache: MultiIdentityTokenCache | None = None,
|
|
80
|
+
) -> tuple[str, CachedIdentity] | None:
|
|
81
|
+
"""Find the cached Datex identity, whatever identity is currently active.
|
|
82
|
+
|
|
83
|
+
Prefers the explicitly selected identity (``--tenant`` override, else the
|
|
84
|
+
persisted active one) when it is itself a Datex identity, so staff with more
|
|
85
|
+
than one Datex account keep control over which is used. Otherwise picks a
|
|
86
|
+
usable one, favouring identities that still hold a refresh token.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
cache: Token cache to read. Defaults to the real credentials file.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
``(storage_key, identity)`` for the chosen identity, or None when the
|
|
93
|
+
cache holds no Datex identity.
|
|
94
|
+
"""
|
|
95
|
+
cache = cache or MultiIdentityTokenCache()
|
|
96
|
+
|
|
97
|
+
candidates = [
|
|
98
|
+
(key, identity)
|
|
99
|
+
for key, identity, _is_active in cache.list_identities()
|
|
100
|
+
if is_datex_identity(identity)
|
|
101
|
+
]
|
|
102
|
+
if not candidates:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
selected_key = get_identity_override()
|
|
106
|
+
if selected_key is None:
|
|
107
|
+
selected_key = next(
|
|
108
|
+
(key for key, _identity, is_active in cache.list_identities() if is_active), None
|
|
109
|
+
)
|
|
110
|
+
for key, identity in candidates:
|
|
111
|
+
if key == selected_key:
|
|
112
|
+
return (key, identity)
|
|
113
|
+
|
|
114
|
+
# No selected Datex identity — prefer one that can still mint tokens.
|
|
115
|
+
candidates.sort(key=lambda c: (not bool(c[1].refresh_token), c[1].is_expired(), c[0]))
|
|
116
|
+
return candidates[0]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def has_datex_identity(cache: MultiIdentityTokenCache | None = None) -> bool:
|
|
120
|
+
"""Whether any cached identity belongs to the Datex tenant."""
|
|
121
|
+
return resolve_datex_identity(cache) is not None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def claims_are_datex(claims: Mapping[str, Any]) -> bool:
|
|
125
|
+
"""Whether a set of token claims describes a Datex staff account.
|
|
126
|
+
|
|
127
|
+
The claims equivalent of :func:`is_datex_identity`, for deciding before an
|
|
128
|
+
identity has been persisted — notably at login, where the consent prompts a
|
|
129
|
+
user sees must not mention Datex-internal resources.
|
|
130
|
+
"""
|
|
131
|
+
tenant_id = claims.get("tid")
|
|
132
|
+
if not tenant_id or str(tenant_id).lower() != _home_tenant_id().lower():
|
|
133
|
+
return False
|
|
134
|
+
username = claims.get("upn") or claims.get("preferred_username") or claims.get("email")
|
|
135
|
+
return not _is_guest_upn(username)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _injected_identity_is_datex() -> bool | None:
|
|
139
|
+
"""Classify an injected ``DXS_ACCESS_TOKEN`` from its own claims.
|
|
140
|
+
|
|
141
|
+
Returns None when no access token is injected, so the caller can fall back
|
|
142
|
+
to inspecting the credentials file.
|
|
143
|
+
"""
|
|
144
|
+
token = os.environ.get("DXS_ACCESS_TOKEN")
|
|
145
|
+
if not token:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
from dxs.core.auth.decorators import _decode_jwt_payload
|
|
149
|
+
|
|
150
|
+
return claims_are_datex(_decode_jwt_payload(token) or {})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def internal_commands_available() -> bool:
|
|
154
|
+
"""Whether the Datex-internal command groups should be exposed.
|
|
155
|
+
|
|
156
|
+
Resolution order: the ``DXS_INTERNAL_COMMANDS`` override, an explicitly
|
|
157
|
+
injected resource token, the claims of an injected ``DXS_ACCESS_TOKEN``,
|
|
158
|
+
then the cached identities.
|
|
159
|
+
"""
|
|
160
|
+
override = os.environ.get(INTERNAL_COMMANDS_ENV, "").strip().lower()
|
|
161
|
+
if override in _TRUTHY:
|
|
162
|
+
return True
|
|
163
|
+
if override in _FALSY:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
if any(os.environ.get(var) for var in INJECTED_RESOURCE_TOKEN_ENV):
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
injected = _injected_identity_is_datex()
|
|
170
|
+
if injected is not None:
|
|
171
|
+
return injected
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
return has_datex_identity()
|
|
175
|
+
except Exception: # noqa: BLE001 - visibility must never break help rendering
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _no_datex_identity_error(resource: str) -> AuthenticationError:
|
|
180
|
+
"""Build the 'this resource needs a Datex login' error."""
|
|
181
|
+
return AuthenticationError(
|
|
182
|
+
message=(
|
|
183
|
+
f"{resource} requires a Datex tenant identity, and none is cached. "
|
|
184
|
+
"This command group is only available to Datex staff."
|
|
185
|
+
),
|
|
186
|
+
code="DXS-AUTH-020",
|
|
187
|
+
suggestions=[
|
|
188
|
+
"Run 'dxs auth login' with your Datex account",
|
|
189
|
+
"Your active identity does not need to change — the Datex identity is "
|
|
190
|
+
"used for this resource automatically",
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def acquire_datex_resource_token(
|
|
196
|
+
scopes: list[str],
|
|
197
|
+
*,
|
|
198
|
+
resource: str,
|
|
199
|
+
error_code: str,
|
|
200
|
+
extra_suggestions: list[str] | None = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
"""Mint an access token for a Datex-internal resource.
|
|
203
|
+
|
|
204
|
+
Redeems the cached Datex identity's refresh token against the Datex tenant
|
|
205
|
+
authority, independently of which identity is active, and persists the
|
|
206
|
+
rotated refresh token back onto that identity.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
scopes: OAuth scopes for the target resource.
|
|
210
|
+
resource: Human-readable resource name, used in error messages.
|
|
211
|
+
error_code: Error code to attach when acquisition fails.
|
|
212
|
+
extra_suggestions: Additional suggestions for the failure path.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
An access token for ``scopes``.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
AuthenticationError: If no Datex identity is cached, or the refresh
|
|
219
|
+
token could not be redeemed.
|
|
220
|
+
"""
|
|
221
|
+
cache = MultiIdentityTokenCache()
|
|
222
|
+
resolved = resolve_datex_identity(cache)
|
|
223
|
+
if resolved is None:
|
|
224
|
+
raise _no_datex_identity_error(resource)
|
|
225
|
+
|
|
226
|
+
identity_key, identity = resolved
|
|
227
|
+
if not identity.refresh_token:
|
|
228
|
+
raise AuthenticationError(
|
|
229
|
+
message=(
|
|
230
|
+
f"Could not acquire {resource} token: the cached Datex identity "
|
|
231
|
+
f"({identity_key}) has no refresh token."
|
|
232
|
+
),
|
|
233
|
+
code=error_code,
|
|
234
|
+
details={"error": "no_refresh_token", "identity": identity_key},
|
|
235
|
+
suggestions=["Run 'dxs auth login' with your Datex account"],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Datex identities are always home-tenant, so the default authority applies.
|
|
239
|
+
client = MSALClient(scopes=scopes)
|
|
240
|
+
result: dict[str, Any] = client.refresh_access_token_detailed(
|
|
241
|
+
refresh_token=identity.refresh_token, scopes=scopes
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if "access_token" in result:
|
|
245
|
+
rotated = result.get("refresh_token")
|
|
246
|
+
if rotated:
|
|
247
|
+
# Keep the Datex identity alive even though it is not the active one.
|
|
248
|
+
cache.update_refresh_token(rotated, username=identity_key)
|
|
249
|
+
return str(result["access_token"])
|
|
250
|
+
|
|
251
|
+
error = result.get("error", "unknown_error")
|
|
252
|
+
error_description = result.get("error_description") or "Token acquisition failed."
|
|
253
|
+
raise AuthenticationError(
|
|
254
|
+
message=f"Could not acquire {resource} token: {error_description}",
|
|
255
|
+
code=error_code,
|
|
256
|
+
details={
|
|
257
|
+
"error": error,
|
|
258
|
+
"error_description": error_description,
|
|
259
|
+
"identity": identity_key,
|
|
260
|
+
},
|
|
261
|
+
suggestions=[
|
|
262
|
+
"Run 'dxs auth login' with your Datex account to refresh credentials",
|
|
263
|
+
*(extra_suggestions or []),
|
|
264
|
+
],
|
|
265
|
+
)
|
|
@@ -151,7 +151,9 @@ class MSALClient:
|
|
|
151
151
|
self,
|
|
152
152
|
primary_scopes: list[str],
|
|
153
153
|
additional_scopes: list[str] | None = None,
|
|
154
|
-
additional_scope_sets: list[list[str]]
|
|
154
|
+
additional_scope_sets: list[list[str]]
|
|
155
|
+
| Callable[[dict[str, Any]], list[list[str]]]
|
|
156
|
+
| None = None,
|
|
155
157
|
on_device_code: Callable[[dict[str, Any]], None] | None = None,
|
|
156
158
|
on_additional_consent_needed: Callable[[str, list[str]], None] | None = None,
|
|
157
159
|
) -> dict[str, Any]:
|
|
@@ -164,7 +166,11 @@ class MSALClient:
|
|
|
164
166
|
Args:
|
|
165
167
|
primary_scopes: Scopes for primary resource (e.g., Datex API)
|
|
166
168
|
additional_scopes: Scopes for a single additional resource (deprecated, use additional_scope_sets)
|
|
167
|
-
additional_scope_sets: List of scope lists for multiple additional
|
|
169
|
+
additional_scope_sets: List of scope lists for multiple additional
|
|
170
|
+
resources, or a callable receiving the primary token response and
|
|
171
|
+
returning that list. The callable form lets the caller choose
|
|
172
|
+
which resources to request consent for based on *who* just
|
|
173
|
+
authenticated — the identity is not known until then.
|
|
168
174
|
on_device_code: Callback to handle the device code response
|
|
169
175
|
on_additional_consent_needed: Callback when additional consent is needed (resource_name, scopes)
|
|
170
176
|
|
|
@@ -222,7 +228,11 @@ class MSALClient:
|
|
|
222
228
|
details={"error": error, "error_description": error_desc},
|
|
223
229
|
)
|
|
224
230
|
|
|
225
|
-
# Build list of all additional scope sets to acquire
|
|
231
|
+
# Build list of all additional scope sets to acquire. A callable is
|
|
232
|
+
# resolved now that the primary identity's claims are known.
|
|
233
|
+
if callable(additional_scope_sets):
|
|
234
|
+
additional_scope_sets = additional_scope_sets(result)
|
|
235
|
+
|
|
226
236
|
all_additional_scopes: list[list[str]] = []
|
|
227
237
|
if additional_scopes:
|
|
228
238
|
all_additional_scopes.append(additional_scopes)
|
|
@@ -16,14 +16,14 @@ DEVOPS_RESOURCE_ID = "499b84ac-1321-427f-aa17-267ca6975798"
|
|
|
16
16
|
def get_devops_token() -> str:
|
|
17
17
|
"""Get an OAuth token for Azure DevOps API.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
Azure DevOps is a Datex-internal resource, so the token is minted from the
|
|
20
|
+
cached Datex-tenant identity rather than from whichever identity is active.
|
|
21
|
+
That keeps ``dxs devops`` working after ``dxs auth switch <customer org>``.
|
|
21
22
|
|
|
22
23
|
Priority:
|
|
23
24
|
0. DXS_DEVOPS_TOKEN environment variable (direct token from external auth)
|
|
24
25
|
1. DXS_REFRESH_TOKEN environment variable (OBO flow)
|
|
25
|
-
2.
|
|
26
|
-
3. File-based refresh token
|
|
26
|
+
2. The cached Datex identity's refresh token
|
|
27
27
|
|
|
28
28
|
Returns:
|
|
29
29
|
Access token for Azure DevOps API
|
|
@@ -33,8 +33,8 @@ def get_devops_token() -> str:
|
|
|
33
33
|
"""
|
|
34
34
|
import os
|
|
35
35
|
|
|
36
|
+
from dxs.core.auth.datex_tenant import acquire_datex_resource_token
|
|
36
37
|
from dxs.core.auth.msal_client import MSALClient
|
|
37
|
-
from dxs.core.auth.token_cache import MultiIdentityTokenCache
|
|
38
38
|
|
|
39
39
|
# Priority 0: DXS_DEVOPS_TOKEN environment variable (direct token)
|
|
40
40
|
# Used when an external tool (e.g., Sidekick) provides a pre-acquired DevOps token
|
|
@@ -48,13 +48,10 @@ def get_devops_token() -> str:
|
|
|
48
48
|
if not devops_scopes:
|
|
49
49
|
devops_scopes = [f"{DEVOPS_RESOURCE_ID}/user_impersonation"]
|
|
50
50
|
|
|
51
|
-
# Track the error from env token attempt for better error reporting
|
|
52
|
-
env_token_error: str | None = None
|
|
53
|
-
env_token_error_desc: str | None = None
|
|
54
|
-
|
|
55
51
|
# Priority 1: DXS_REFRESH_TOKEN environment variable (OBO flow)
|
|
56
52
|
env_refresh_token = os.environ.get("DXS_REFRESH_TOKEN")
|
|
57
53
|
if env_refresh_token:
|
|
54
|
+
env_error: dict[str, Any]
|
|
58
55
|
try:
|
|
59
56
|
msal_client = MSALClient(scopes=devops_scopes)
|
|
60
57
|
result = msal_client._app.acquire_token_by_refresh_token(
|
|
@@ -65,67 +62,42 @@ def get_devops_token() -> str:
|
|
|
65
62
|
return cast(str, result["access_token"])
|
|
66
63
|
# Capture MSAL error response for reporting
|
|
67
64
|
if result:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"error_description"
|
|
71
|
-
|
|
65
|
+
env_error = {
|
|
66
|
+
"error": result.get("error", "unknown_error"),
|
|
67
|
+
"error_description": result.get(
|
|
68
|
+
"error_description", "OBO token acquisition failed"
|
|
69
|
+
),
|
|
70
|
+
}
|
|
72
71
|
else:
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
env_error = {
|
|
73
|
+
"error": "no_result",
|
|
74
|
+
"error_description": "No response from OBO token refresh",
|
|
75
|
+
}
|
|
75
76
|
except Exception as e:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return cast(str, result["access_token"])
|
|
101
|
-
|
|
102
|
-
error = result.get("error") if result else "no_result"
|
|
103
|
-
error_desc = (
|
|
104
|
-
result.get("error_description") if result else "No response from token refresh"
|
|
105
|
-
)
|
|
106
|
-
except Exception as e:
|
|
107
|
-
error = "exception"
|
|
108
|
-
error_desc = str(e)
|
|
109
|
-
else:
|
|
110
|
-
# If env token was tried but failed, report that error instead
|
|
111
|
-
if env_token_error:
|
|
112
|
-
error = env_token_error
|
|
113
|
-
error_desc = env_token_error_desc
|
|
114
|
-
else:
|
|
115
|
-
error = "no_refresh_token"
|
|
116
|
-
error_desc = (
|
|
117
|
-
"No refresh token available (set DXS_REFRESH_TOKEN or run 'dxs auth login')"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
raise AuthenticationError(
|
|
121
|
-
message=f"Could not acquire Azure DevOps token: {error_desc}",
|
|
122
|
-
code="DXS-DEVOPS-AUTH-001",
|
|
123
|
-
details={"error": error, "error_description": error_desc},
|
|
124
|
-
suggestions=[
|
|
125
|
-
"Set DXS_REFRESH_TOKEN environment variable for OBO flow",
|
|
126
|
-
"Run 'dxs auth login' to authenticate with refresh token",
|
|
127
|
-
"Ensure your account has Azure DevOps access",
|
|
128
|
-
],
|
|
77
|
+
env_error = {
|
|
78
|
+
"error": "exception",
|
|
79
|
+
"error_description": f"OBO token acquisition failed: {e}",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# An injected refresh token is an explicit instruction about which
|
|
83
|
+
# credential to use; do not silently fall back to a cached identity.
|
|
84
|
+
raise AuthenticationError(
|
|
85
|
+
message=f"Could not acquire Azure DevOps token: {env_error['error_description']}",
|
|
86
|
+
code="DXS-DEVOPS-AUTH-001",
|
|
87
|
+
details=env_error,
|
|
88
|
+
suggestions=[
|
|
89
|
+
"Verify DXS_REFRESH_TOKEN is current and consented for Azure DevOps",
|
|
90
|
+
"Run 'dxs auth login' to authenticate with refresh token",
|
|
91
|
+
"Ensure your account has Azure DevOps access",
|
|
92
|
+
],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Priority 2: The cached Datex identity, whichever identity is active.
|
|
96
|
+
return acquire_datex_resource_token(
|
|
97
|
+
devops_scopes,
|
|
98
|
+
resource="Azure DevOps",
|
|
99
|
+
error_code="DXS-DEVOPS-AUTH-001",
|
|
100
|
+
extra_suggestions=["Ensure your account has Azure DevOps access"],
|
|
129
101
|
)
|
|
130
102
|
|
|
131
103
|
|