microsoft / microsoft/simplechat

Admin settings secret resolution is asymmetric: nested-path secrets can't use the route-level resolve

Open
#1,429 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
152
Forks
116
Avg merge
7h 7m
Merged PRs (30d)
122

Description

## Summary

Admin settings credentials are resolved in two different places depending on where the value is stored, and the split is invisible from the field declaration. A flat secret is resolved in the V2 route; a secret declared with `paths` cannot be, and is handled in the schema normalizer instead. Anyone adding a nested secret field has to know this, and nothing enforces it.

This is not currently a bug — both paths work — but it is the kind of asymmetry that rots. Filing it rather than unifying it now, because the fix touches shared code that several admin-settings PRs are queued against.

## Why the two paths exist

`resolve_admin_settings_secret_value(field_name, submitted, existing)` in `admin_settings_secret_utils.py` swaps a submitted placeholder back for the stored credential, so an untouched secret survives a save. `route_backend_v2.py` applies it over the submitted keys in the PATCH.

That pass works by **settings key**. A field declaring `paths` is folded into its containing object by `_apply_nested_paths` in `admin_settings_fields.py` *before* the route sees the result, so it never arrives as a key of its own. The Web Search Foundry client secret is the live case:

| | |
|---|---|
| Field key | `web_search_foundry_client_secret` |
| Declared `paths` | `web_search_agent.other_settings.azure_ai_foundry.client_secret` |
| What the route sees | `web_search_agent` (a whole object) |

So the route cannot reach it. Without something else handling it, an untouched placeholder would be folded into the container and written over the real credential — saving any unrelated Web Search setting would destroy the stored secret.

The current handling is in `_normalize_field_value`: a submitted placeholder is dropped **only when the field declares `paths`**. Dropping is sufficient because `_apply_nested_paths` rebuilds the container from stored settings, so the stored secret is carried through untouched. Flat secrets are left entirely to the route.

## Why it's worth unifying

1. **The rule is undiscoverable.** Nothing tells the author of the next nested secret field that declaring `paths` is what makes it safe. Declaring a nested secret *without* `paths`, or resolving it in the route by field key, silently does nothing.
2. **`resolve_admin_settings_secret_value` already supports it.** It reads through `get_nested_setting_value`, which takes a dotted path. The helper is capable; only the caller lacks the path.
3. **Two overlapping redaction lists.** The GET redacts through both `ADMIN_SETTINGS_NESTED_SECRET_FIELDS` (a hand-maintained tuple that already lists the Foundry path) and `get_secret_storage_paths()` (derived from the schema). Both cover this key today. The derived one generalizes; the tuple does not.

## Suggested fix

Resolve secrets by **storage location** rather than by settings key, before `_apply_nested_paths` folds anything:

- Have the route ask the schema for `field key -> storage path` for every declared secret.
- Call `resolve_admin_settings_secret_value(storage_path, submitted, settings)` for each, so nested and flat secrets take the same route-level path.
- Remove the `paths` exception from `_normalize_field_value`.
- Consider deriving `ADMIN_SETTINGS_NESTED_SECRET_FIELDS` from the schema's declared storage paths instead of maintaining it by hand, so declaring a nested secret protects it on read without a second edit.

Then add a test asserting that every schema-declared secret, flat or nested, survives a save in which it was not touched. That is the property that actually matters and it currently has no single test covering both shapes.

## Files involved

- `application/single_app/admin_settings_secret_utils.py` — `resolve_admin_settings_secret_value`, `ADMIN_SETTINGS_NESTED_SECRET_FIELDS`, `get_admin_settings_api_secret_fields`
- `application/single_app/admin_settings_fields.py` — `_normalize_field_value` (the `paths` exception), `_apply_nested_paths`, `get_secret_storage_paths`
- `application/single_app/route_backend_v2.py` — `_redact_admin_settings_for_v2`, the PATCH resolve pass

## Context

Found while merging #1425 (Knowledge admin settings V2) against the Security (#1421) and Chat (#1422) work, which introduced the shared mechanism. Discussed with the PR merge orchestration session, which agreed the unification is the better end state but should not happen inside a merge resolve with PRs queued behind it.

Refs #1425

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read resolve_admin_settings_secret_value and get_secret_storage_paths in application/single_app/admin_settings_secret_utils.py and admin_settings_fields.py, then trace the PATCH resolve pass and _redact_admin_settings_for_v2 in route_backend_v2.py. Add coverage for untouched flat and nested schema-declared secrets surviving a save, and verify the existing admin-settings tests pass with one storage-location resolution path.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, security
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.