azure-dev-proxy 0.1.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.
Files changed (33) hide show
  1. azure_dev_proxy-0.1.0/.github/workflows/publish.yml +64 -0
  2. azure_dev_proxy-0.1.0/.github/workflows/tests.yml +29 -0
  3. azure_dev_proxy-0.1.0/.gitignore +10 -0
  4. azure_dev_proxy-0.1.0/LICENSE +21 -0
  5. azure_dev_proxy-0.1.0/PKG-INFO +8 -0
  6. azure_dev_proxy-0.1.0/README.md +258 -0
  7. azure_dev_proxy-0.1.0/azure_dev_proxy/__init__.py +1 -0
  8. azure_dev_proxy-0.1.0/azure_dev_proxy/__main__.py +104 -0
  9. azure_dev_proxy-0.1.0/azure_dev_proxy/auth.py +382 -0
  10. azure_dev_proxy-0.1.0/azure_dev_proxy/config.py +367 -0
  11. azure_dev_proxy-0.1.0/azure_dev_proxy/default-config.json +34 -0
  12. azure_dev_proxy-0.1.0/azure_dev_proxy/idle_timer.py +62 -0
  13. azure_dev_proxy-0.1.0/azure_dev_proxy/listeners.py +108 -0
  14. azure_dev_proxy-0.1.0/azure_dev_proxy/proxy.py +478 -0
  15. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/__init__.py +3 -0
  16. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/_base.py +31 -0
  17. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/graphiql.py +96 -0
  18. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/management.py +247 -0
  19. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/reverse_proxy.py +174 -0
  20. azure_dev_proxy-0.1.0/azure_dev_proxy/routers/utils.py +220 -0
  21. azure_dev_proxy-0.1.0/azure_dev_proxy/static/app.js +124 -0
  22. azure_dev_proxy-0.1.0/azure_dev_proxy/static/graphiql.html +109 -0
  23. azure_dev_proxy-0.1.0/azure_dev_proxy/static/index.html +90 -0
  24. azure_dev_proxy-0.1.0/azure_dev_proxy/static/login.html +26 -0
  25. azure_dev_proxy-0.1.0/azure_dev_proxy/tokens.py +242 -0
  26. azure_dev_proxy-0.1.0/pyproject.toml +52 -0
  27. azure_dev_proxy-0.1.0/tests/conftest.py +42 -0
  28. azure_dev_proxy-0.1.0/tests/fake_az.py +19 -0
  29. azure_dev_proxy-0.1.0/tests/test_auth.py +773 -0
  30. azure_dev_proxy-0.1.0/tests/test_config.py +457 -0
  31. azure_dev_proxy-0.1.0/tests/test_features.py +1116 -0
  32. azure_dev_proxy-0.1.0/tests/test_proxy.py +1823 -0
  33. azure_dev_proxy-0.1.0/tests/test_tokens.py +478 -0
@@ -0,0 +1,64 @@
1
+ name: Publish tagged releases to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ tests:
13
+ uses: ./.github/workflows/tests.yml
14
+
15
+ build:
16
+ needs: tests
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v6
22
+ with:
23
+ python-version: "3.14"
24
+ - name: Check release version
25
+ env:
26
+ RELEASE_TAG: ${{ github.ref_name }}
27
+ run: |
28
+ python - <<'PY'
29
+ import os
30
+ import tomllib
31
+ from pathlib import Path
32
+
33
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
34
+ expected = f"v{version}"
35
+ if os.environ["RELEASE_TAG"] != expected:
36
+ raise SystemExit(f"Release tag must be {expected}")
37
+ PY
38
+ - name: Install Hatch
39
+ run: python -m pip install hatch
40
+ - name: Build distributions
41
+ run: hatch build
42
+ - name: Upload distributions
43
+ uses: actions/upload-artifact@v6
44
+ with:
45
+ name: python-distributions
46
+ path: dist/
47
+ if-no-files-found: error
48
+
49
+ publish:
50
+ needs: build
51
+ runs-on: ubuntu-latest
52
+ environment:
53
+ name: pypi
54
+ url: https://pypi.org/p/azure-dev-proxy
55
+ permissions:
56
+ id-token: write
57
+ steps:
58
+ - name: Download distributions
59
+ uses: actions/download-artifact@v6
60
+ with:
61
+ name: python-distributions
62
+ path: dist/
63
+ - name: Publish to PyPI
64
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ name: Run tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+ pull_request:
8
+ workflow_call:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v6
24
+ with:
25
+ python-version: ${{ matrix.python-version }}
26
+ - name: Install Hatch
27
+ run: python -m pip install hatch
28
+ - name: Run tests and lint
29
+ run: hatch run tests
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ *.egg-info/
5
+ dist/
6
+ .pytest_cache/
7
+ .coverage
8
+ .coverage.*
9
+ htmlcov/
10
+ coverage.xml
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Wardrop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.5
2
+ Name: azure-dev-proxy
3
+ Version: 0.1.0
4
+ Summary: Loopback HTTP(S) proxy with HTTPS-only Azure CLI authentication
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: aiohttp<4,>=3.14
8
+ Requires-Dist: idna<4,>=3.7
@@ -0,0 +1,258 @@
1
+ # Azure Dev Proxy
2
+
3
+ A reverse proxy that allows you to open APIs or webpages using your existing
4
+ Azure CLI sign-in. It forwards requests through a local loopback endpoint and
5
+ injects `Authorization: Bearer` tokens for configured HTTPS destinations.
6
+
7
+ The browser UI provides URL navigation, host-to-token-audience mappings, and
8
+ GraphiQL. Command-line clients use the same forwarding routes. HTTP destinations
9
+ are supported, but never receive an Azure token.
10
+
11
+ ## Get started
12
+
13
+ You need Python 3.11 or newer and Azure CLI available as `az` on your path.
14
+ Install from PyPI, sign in, and start the proxy as a non-root user:
15
+
16
+ ```sh
17
+ python -m pip install azure-dev-proxy
18
+ az login
19
+ azure-dev-proxy
20
+ ```
21
+
22
+ The proxy listens on loopback interfaces at port 8080, not on the local network.
23
+ Shared-key authentication protects access to the proxy separately from upstream
24
+ Azure authentication. On first use, it creates an `auth-token` file beside its
25
+ configuration and reuses the key across restarts.
26
+
27
+ ### Browser
28
+
29
+ 1. Open a ready-to-use URL from the startup logs, or visit
30
+ `http://localhost:8080/` and enter the key.
31
+ 2. Add a [host mapping](#configure-host-mappings) if your service is not covered
32
+ by the defaults.
33
+ 3. Paste the destination URL and select **Open URL**.
34
+
35
+ ### curl
36
+
37
+ With the proxy running, open another terminal. In a POSIX shell, replace
38
+ `/path/to/auth-token` with the key-file path printed at startup and send a GET
39
+ request:
40
+
41
+ ```sh
42
+ PROXY_KEY=$(cat /path/to/auth-token)
43
+
44
+ curl --fail-with-body \
45
+ -H "X-Azure-Dev-Proxy-Token: $PROXY_KEY" \
46
+ "http://127.0.0.1:8080/https://api.example.com/path"
47
+ ```
48
+
49
+ To POST JSON:
50
+
51
+ ```sh
52
+ curl --fail-with-body \
53
+ -H "X-Azure-Dev-Proxy-Token: $PROXY_KEY" \
54
+ -H "Content-Type: application/json" \
55
+ --data '{"name":"example"}' \
56
+ "http://127.0.0.1:8080/https://api.example.com/items"
57
+ ```
58
+
59
+ Replace the example URLs with your API and configure a matching host mapping
60
+ if it needs an Azure token. If you use `--config` or `--token-file`, read the
61
+ key file shown at startup instead.
62
+
63
+ The header authenticates to the local proxy; the host mapping selects the
64
+ upstream bearer token. Embed the destination URL in the request path rather
65
+ than using `curl --proxy`. Use `127.0.0.1` to avoid the `localhost` redirect.
66
+
67
+ > [!WARNING]
68
+ > **Keep the proxy key and startup logs private.** Startup URLs intentionally
69
+ > include the key for convenient sign-in. Anyone with the key and access to the
70
+ > local listener can use your Azure identity through the proxy. Keys can also
71
+ > appear in browser history, command traces, or process listings. The proxy does
72
+ > not isolate malicious applications running as your own user.
73
+
74
+ ## Configure host mappings
75
+
76
+ A host mapping allowlists an HTTPS destination for token injection and selects
77
+ the token resource, or audience, requested through Azure CLI. Use the audience
78
+ required by the API; it may differ from the API's URL.
79
+
80
+ On `localhost`, expand **Host mappings and temporary overrides**, enter a
81
+ hostname and resource, and select **Save temporary override**. A blank resource
82
+ uses the destination's HTTPS origin.
83
+
84
+ Temporary rules apply immediately and disappear on process exit. **Save to
85
+ persisted** writes a rule to the configuration file; **Delete persisted**
86
+ removes a saved rule. Persisted edits require a writable file and apply without
87
+ a restart. Failed writes leave the active rules unchanged.
88
+
89
+ Alternatively, supply JSON:
90
+
91
+ ```json
92
+ {
93
+ "token_rules": [
94
+ {"host": "api.example.com", "resource": "https://identity.example.com"},
95
+ {"host": "*.example.org"}
96
+ ],
97
+ "trusted_origins": []
98
+ }
99
+ ```
100
+
101
+ ```sh
102
+ azure-dev-proxy --config ./config.json
103
+ azure-dev-proxy --config ./config.json --check-config
104
+ ```
105
+
106
+ Replace the placeholder domains with your service settings. Exact hosts take
107
+ priority over wildcards; the longest wildcard suffix wins. Wildcards match
108
+ subdomains, not the bare domain. A missing or `null` resource uses the
109
+ destination's HTTPS origin. Unmatched hosts are forwarded without an Azure
110
+ token. Temporary overrides take priority over persisted rules.
111
+
112
+ Configuration uses the native per-user directory:
113
+
114
+ | Platform | Default configuration file |
115
+ |---|---|
116
+ | Linux | `$XDG_CONFIG_HOME/azure-dev-proxy/config.json`, or `~/.config/azure-dev-proxy/config.json` when unset. |
117
+ | Windows | `%APPDATA%\azure-dev-proxy\config.json`, or `~\AppData\Roaming\azure-dev-proxy\config.json` when unset. |
118
+ | macOS | `~/Library/Application Support/azure-dev-proxy/config.json` |
119
+
120
+ The generated `auth-token` lives beside the configuration unless `--token-file`
121
+ is supplied. `XDG_CONFIG_HOME` applies only on Linux and other non-Windows,
122
+ non-macOS platforms. Missing files receive bundled defaults; existing files
123
+ define the complete rule list and are never merged with updated defaults.
124
+
125
+ On Windows and macOS, existing files under `~/.config` are not moved
126
+ automatically. Use `--config` with the old path to retain those settings and
127
+ the adjacent key, or move both files to the native directory.
128
+
129
+ ### Bundled mappings
130
+
131
+ The [default configuration](azure_dev_proxy/default-config.json) includes
132
+ public-cloud mappings for management, storage, secrets, messaging, analytics,
133
+ AI, monitoring, and productivity APIs. It is a curated allowlist, not a
134
+ wildcard for every Azure-hosted service. Your signed-in user still needs the
135
+ service's permissions and consent; supply any additional API-required headers.
136
+
137
+ Event Hubs and Service Bus share `*.servicebus.windows.net` but require
138
+ different audiences. The default selects Service Bus. For Event Hubs, add an
139
+ exact hostname override with resource `https://eventhubs.azure.net`.
140
+
141
+ ## Forwarding and GraphQL
142
+
143
+ Forwarding routes embed the upstream URL in the local path:
144
+
145
+ ```text
146
+ http://127.0.0.1:8080/https://api.example.com/path
147
+ http://127.0.0.1:8080/http://localhost:4000/path
148
+ ```
149
+
150
+ The URL-entry page defaults to HTTPS when the scheme is omitted. Forwarding
151
+ routes on `localhost` redirect to `127.0.0.1`, separating upstream pages from
152
+ the configuration editor. Only `localhost` permits configuration edits;
153
+ IP-loopback origins expose read-only mappings.
154
+
155
+ For GraphiQL, open:
156
+
157
+ ```text
158
+ http://localhost:8080/:graphiql/https://api.example.com/graphql
159
+ ```
160
+
161
+ ### Browser applications
162
+
163
+ To permit cross-origin forwarding from a local application, add its exact
164
+ origin to `trusted_origins`, for example `["http://localhost:3000"]`. Include
165
+ nondefault ports; paths, wildcards, and opaque (`null`) origins are rejected.
166
+ Only trusted origins receive forwarding CORS headers.
167
+
168
+ The application must also authenticate with `X-Azure-Dev-Proxy-Token`; origin
169
+ trust does not replace authentication or enable cross-site session cookies.
170
+ By default, only the proxy's own loopback origins are trusted.
171
+
172
+ **Trust only applications and forwarded pages that may use your Azure
173
+ identity.** Forwarded pages share the IP-loopback origin; host mappings do not
174
+ isolate those pages from one another.
175
+
176
+ ## Security and sessions
177
+
178
+ Azure tokens are injected only for mapped HTTPS destinations. Redirects are
179
+ evaluated independently, and HTTP forwarding is always anonymous. Caller
180
+ `Authorization` headers, cookies, and proxy credentials are stripped upstream.
181
+ Request and response bodies are streamed without automatic decompression.
182
+
183
+ Browser-origin checks run before forwarding or token acquisition. Configuration
184
+ mutations additionally require same-origin access and an anti-CSRF token.
185
+ Browsers must support Fetch Metadata. Clients without browser provenance
186
+ headers are treated as CLI clients but still require proxy authentication.
187
+
188
+ The reserved `azure-dev-proxy-token=KEY` query parameter also authenticates
189
+ requests and establishes a session. It is stripped before forwarding.
190
+ Browser navigations redirect to a key-free URL; API requests receive their
191
+ response directly. Other query parameters retain their encoding and order.
192
+
193
+ Sessions use HttpOnly, SameSite=Lax, host-only cookies. A short-lived, single-use
194
+ handoff pairs sessions across `localhost` and `127.0.0.1`. Sessions can survive
195
+ restarts with the same key and port; rotate the key and restart to invalidate
196
+ them.
197
+
198
+ On POSIX, the key must be a regular file owned by the current user with no
199
+ group or other permissions. Symlinks are rejected, and validation and reading
200
+ use the same file descriptor to prevent pathname-replacement races. On Windows,
201
+ protect the containing directory's inherited permissions. Request logs omit
202
+ the reserved key parameter; startup logs intentionally include it.
203
+
204
+ ## Runtime options
205
+
206
+ | Option | Purpose |
207
+ |---|---|
208
+ | `--port 8080` | Set the loopback port. |
209
+ | `--az-path /path/to/az` | Select the Azure CLI executable; defaults to `az` on `PATH`. |
210
+ | `--config FILE` | Select a JSON configuration file. |
211
+ | `--check-config` | Validate configuration without listening; creates defaults if missing. |
212
+ | `--idle-timeout 300` | Exit after five idle minutes; default `0` disables idle shutdown. |
213
+ | `--token-file FILE` | Select a protected key file. |
214
+ | `--token KEY` | Supply the proxy key directly. |
215
+ | `--no-auth` | Disable proxy authentication and key-file creation. |
216
+
217
+ `AZURE_DEV_PROXY_TOKEN` also supplies a key; explicit `--token` or
218
+ `--token-file` options take precedence. **With `--no-auth`, other local users
219
+ and applications can use the proxy without a key.**
220
+
221
+ Active requests and streams prevent idle shutdown; open pages and unused
222
+ keep-alive connections do not. Temporary rules and acquired tokens are
223
+ memory-only.
224
+
225
+ Systemd socket activation accepts one or two loopback TCP listeners through
226
+ `LISTEN_PID` and `LISTEN_FDS`; their ports must match `--port`. Otherwise, the
227
+ proxy binds IPv4 and available IPv6 loopback interfaces.
228
+
229
+ ## Development
230
+
231
+ From a local checkout:
232
+
233
+ ```sh
234
+ python -m pip install -e .
235
+ ```
236
+
237
+ Install [Hatch](https://hatch.pypa.io/latest/install/), then run:
238
+
239
+ ```sh
240
+ hatch run tests
241
+ hatch run format
242
+ hatch build
243
+ ```
244
+
245
+ `tests` runs pytest with a 100% Python line-and-branch coverage gate, including
246
+ CLI subprocesses, followed by Ruff lint and format checks and ty type checking.
247
+ `format` applies Ruff fixes and formatting. For a focused run, use
248
+ `hatch run unit-test -k token -v`.
249
+
250
+ Use `hatch run coverage` for the coverage report alone. While adding tests,
251
+ `hatch run coverage --cov-fail-under=0` reports remaining gaps without enforcing
252
+ the gate. Add `--cov-report=html` for a browsable report in `htmlcov/`.
253
+
254
+ The import namespace is `azure_dev_proxy`. Configuration, authentication, token
255
+ acquisition, and listeners are separate modules. `Proxy` loads configuration,
256
+ initializes the token provider and local authentication, and owns the aiohttp
257
+ application and lifecycle, with routes in `azure_dev_proxy.routers`. Callers
258
+ can also supply configuration, a token provider, or authentication explicitly.
@@ -0,0 +1 @@
1
+ """Local Azure-authenticated HTTPS reverse proxy."""
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from .config import Config
9
+ from .proxy import Proxy
10
+
11
+
12
+ def port_number(value: str) -> int:
13
+ try:
14
+ port = int(value)
15
+ except ValueError as error:
16
+ raise argparse.ArgumentTypeError("Port must be an integer") from error
17
+ if not 1 <= port <= 65535:
18
+ raise argparse.ArgumentTypeError("Port must be between 1 and 65535")
19
+ return port
20
+
21
+
22
+ def main() -> None:
23
+ """
24
+ Run the loopback proxy or validate its configuration without listening.
25
+
26
+ POSIX supports inherited systemd listeners; otherwise the service binds its
27
+ own loopback sockets. Windows uses direct listeners and native signal handling.
28
+ """
29
+ parser = argparse.ArgumentParser(
30
+ prog="azure-dev-proxy",
31
+ description="Local HTTP(S) proxy with HTTPS-only Azure CLI tokens",
32
+ )
33
+ parser.add_argument("--version", action="version", version="azure-dev-proxy 0.1.0")
34
+ parser.add_argument(
35
+ "--port", type=port_number, default=8080, help="Loopback port (default: 8080)"
36
+ )
37
+ parser.add_argument(
38
+ "--az-path",
39
+ default="az",
40
+ help="Azure CLI executable path (default: az on PATH)",
41
+ )
42
+ parser.add_argument(
43
+ "--config",
44
+ type=Path,
45
+ help="JSON configuration (default: the user's azure-dev-proxy/config.json)",
46
+ )
47
+ parser.add_argument(
48
+ "--idle-timeout",
49
+ type=idle_seconds,
50
+ default=0,
51
+ help="Opt-in idle shutdown seconds (default: 0, disabled)",
52
+ )
53
+ auth = parser.add_mutually_exclusive_group()
54
+ auth.add_argument(
55
+ "--token", help="Proxy authentication key (or AZURE_DEV_PROXY_TOKEN)"
56
+ )
57
+ auth.add_argument(
58
+ "--token-file",
59
+ type=Path,
60
+ help="Saved key file (default: auth-token beside the configuration)",
61
+ )
62
+ auth.add_argument(
63
+ "--no-auth", action="store_true", help="Disable proxy authentication"
64
+ )
65
+ parser.add_argument(
66
+ "--check-config",
67
+ action="store_true",
68
+ help="Validate configuration and exit without listening",
69
+ )
70
+ args = parser.parse_args()
71
+ logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
72
+ try:
73
+ if not args.check_config and hasattr(os, "geteuid") and os.geteuid() == 0:
74
+ raise ValueError("Run azure-dev-proxy as a non-root user")
75
+ if args.check_config:
76
+ Config.load(
77
+ args.config if args.config is not None else Config.default_path()
78
+ )
79
+ return
80
+ proxy = Proxy(
81
+ config_path=args.config,
82
+ az_path=args.az_path,
83
+ token=args.token,
84
+ token_file=args.token_file,
85
+ no_auth=args.no_auth,
86
+ )
87
+ proxy.serve(args.port, idle_timeout=args.idle_timeout)
88
+ except (OSError, ValueError, RuntimeError) as error:
89
+ parser.exit(1, f"Could not start azure-dev-proxy: {error}\n")
90
+
91
+
92
+ def idle_seconds(value: str) -> int:
93
+ """Parse nonnegative idle seconds; zero disables automatic shutdown."""
94
+ try:
95
+ seconds = int(value)
96
+ except ValueError:
97
+ raise argparse.ArgumentTypeError("Idle timeout must be an integer") from None
98
+ if seconds < 0:
99
+ raise argparse.ArgumentTypeError("Idle timeout must be nonnegative")
100
+ return seconds
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()