Skip to content

feat(auth): add per-call AWS profile override middleware#205

Draft
benjstoll wants to merge 3 commits intoaws:mainfrom
benjstoll:feat/profile-override-middleware
Draft

feat(auth): add per-call AWS profile override middleware#205
benjstoll wants to merge 3 commits intoaws:mainfrom
benjstoll:feat/profile-override-middleware

Conversation

@benjstoll
Copy link
Copy Markdown

@benjstoll benjstoll commented Mar 24, 2026

Adds ProfileOverrideMiddleware that allows routing individual tool calls through dedicated per-profile MCP connections via a profile argument. Enabled with --allow-switch-profile CLI flag restricted to an explicit allowlist of profile names.

Summary

Changes

  • Added ProfileOverrideMiddleware in mcp_proxy_for_aws/middleware/profile_switcher.py that intercepts a profile argument on any tool call,
    validates it against an allowlist, and routes the request through a dedicated per-profile MCP client with its own SigV4-signed transport.
  • Added --allow-switch-profile CLI argument in cli.py that accepts one or more AWS profile names to enable the middleware.
  • Wired the middleware into server.py with proper lifecycle management (lazy client creation, graceful shutdown of per-profile connections
    in the finally block).
  • Added 12 unit tests covering tool schema injection, pass-through behavior, disallowed profiles, argument stripping, connection/tool-call
    error handling, and client disconnect logic. 90% branch coverage on the new middleware.
  • Updated README.md with the new parameter in the configuration table and a "Multi-account access" section explaining how
    --allow-switch-profile interacts with --profile, including a JSON config example.

User experience

Before: Users who needed to query AWS resources across multiple accounts had to run separate proxy instances per profile, or manually
restart the proxy with a different --profile value.

After: Users pass --allow-switch-profile profile-a profile-b alongside their default --profile. Any tool call can include a profile
argument to route that single request through a dedicated connection signed with the specified profile's credentials. Tool calls without
profile continue to use the default connection. Each profile's connection is created lazily on first use, so there is no startup cost for
unused profiles.

Checklist

If your change doesn't seem to apply, please leave them unchecked.

  • I have reviewed the contributing guidelines
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Is this a breaking change? (Y/N)

  • Yes
  • No

Please add details about how this change was tested.

  • Did integration tests succeed?
  • If the feature is a new use case, is it necessary to add a new integration test case?

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Adds ProfileOverrideMiddleware that allows routing individual tool calls
through dedicated per-profile MCP connections via a `profile` argument.
Enabled with `--allow-switch-profile` CLI flag restricted to an explicit
allowlist of profile names.
@benjstoll benjstoll force-pushed the feat/profile-override-middleware branch 2 times, most recently from 4476898 to f142bea Compare April 6, 2026 12:56
Concurrent tool calls (e.g. from parallel subagents) could race in
_get_profile_client, each creating a separate Client for the same
profile. The loser's client would leak — connected but never tracked
or cleaned up. Wrapping in an asyncio.Lock ensures only one client
is created per profile.
@benjstoll benjstoll force-pushed the feat/profile-override-middleware branch from f142bea to 1ba8d99 Compare April 6, 2026 12:57
"""Intercept ``profile`` and route through a dedicated per-profile client."""
arguments = context.message.arguments
if isinstance(arguments, dict) and 'profile' in arguments:
profile = arguments['profile']
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

profile can collide with tools that already have a profile parameter. If a backend tool already has a profile parameter in its schema, this middleware will silently overwrite it in on_list_tools and strip it in on_call_tool. This could break legitimate tool parameters.

"""Forward a tool call through a dedicated per-profile connection."""
if profile not in self._allowed_profiles:
allowed = ', '.join(sorted(self._allowed_profiles))
return ToolResult(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you return ToolResult(s) on failed validations instead of raising ToolError?

Comment on lines +105 to +116
allowed_profiles = getattr(args, 'allow_switch_profile', None)
if isinstance(allowed_profiles, list) and allowed_profiles:
profile_middleware = ProfileOverrideMiddleware(
allowed_profiles=allowed_profiles,
service=service,
region=region,
metadata=metadata,
timeout=timeout,
endpoint=args.endpoint,
)
proxy.add_middleware(profile_middleware)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be extracted into a separate helper add_profile_override_middleware, to stay consistent with other middleware declarations

continue
if 'properties' not in params:
params['properties'] = {}
params['properties']['profile'] = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the upstream tool list is cached or shared, this mutates shared state. This is ok now because AWSProxyToolManager.get_tools() re-fetches, however this creates dependency on the upstream to always produce fresh dicts. I'd suggest deep-copy the parameters dict before mutating. It is cheap insurance and makes the middleware self-contained.

"""Inject ``profile`` into every tool's schema."""
tools = await call_next(context)

for tool in tools:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if we could use tool transformations instead of directly manipulating params?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

switch_profile tool to allow multi-aws account access Impossible to use 2 distinct profiles in a single Kiro CLI session.

4 participants