Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions src/PowerPlatform/Dataverse/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,48 @@
# Values: alphanumeric, hyphens, underscores, dots, slashes.
_CONTEXT_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+=[a-zA-Z0-9_./-]+(;[a-zA-Z0-9_-]+=[a-zA-Z0-9_./-]+)*$")

# Allowed keys and their value patterns for PII prevention.
# Only these keys are accepted; unknown keys are rejected.
_ALLOWED_KEYS = frozenset({"app", "skill", "agent"})
_ALLOWED_SKILLS = frozenset(
{
"dv-connect",
"dv-data",
"dv-query",
"dv-metadata",
"dv-solution",
"dv-admin",
"dv-security",
}
)
_ALLOWED_AGENTS = frozenset(
{
"claude-code",
"copilot",
"cursor",
"codex",
"unknown",
}
)
# app values: must start with a known prefix followed by /<semver-like>
_APP_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+/[a-zA-Z0-9_./-]+$")


@dataclass(frozen=True)
class OperationContext:
"""Caller-defined context appended to outbound ``User-Agent`` headers.

The context string is validated to be semicolon-separated ``key=value`` pairs
(e.g. ``"app=myapp/1.0;agent=claude-code"``). Free-form text, email
addresses, and other potentially sensitive strings are rejected.
using only allowed keys (``app``, ``skill``, ``agent``) with values from
closed allowlists. Free-form text, email addresses, PII, and unknown keys
are rejected.

:param user_agent_context: Attribution string in ``key=value;key=value`` format.
:type user_agent_context: :class:`str`

:raises ValueError: If the string is empty, contains control characters, or
does not match the required ``key=value`` format.
:raises ValueError: If the string is empty, contains control characters,
does not match the required ``key=value`` format, or uses unknown
keys/values.
"""

user_agent_context: str
Expand All @@ -54,6 +82,17 @@ def __post_init__(self) -> None:
"Keys and values may contain alphanumerics, hyphens, underscores, "
"dots, and slashes."
)
# Key/value allowlist validation
for pair in val.split(";"):
key, _, value = pair.partition("=")
if key not in _ALLOWED_KEYS:
raise ValueError(f"Unknown operation_context key '{key}'. " f"Allowed keys: {sorted(_ALLOWED_KEYS)}")
if key == "skill" and value not in _ALLOWED_SKILLS:
raise ValueError(f"Unknown skill '{value}'. Allowed: {sorted(_ALLOWED_SKILLS)}")
if key == "agent" and value not in _ALLOWED_AGENTS:
raise ValueError(f"Unknown agent '{value}'. Allowed: {sorted(_ALLOWED_AGENTS)}")
if key == "app" and not _APP_PATTERN.match(value):
raise ValueError(f"Invalid app value '{value}'. Expected format: '<name>/<version>'.")


@dataclass(frozen=True)
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_operation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ def test_reject_no_equals(self):
with self.assertRaises(ValueError):
OperationContext(user_agent_context="justaplainstring")

def test_reject_unknown_key(self):
with self.assertRaises(ValueError):
OperationContext(user_agent_context="ssn=123-45-6789")

def test_reject_unknown_skill(self):
with self.assertRaises(ValueError):
OperationContext(user_agent_context="app=test/1.0;skill=not-a-real-skill;agent=claude-code")

def test_reject_unknown_agent(self):
with self.assertRaises(ValueError):
OperationContext(user_agent_context="app=test/1.0;skill=dv-data;agent=not-a-real-agent")

def test_reject_pii_in_valid_key_format(self):
"""Even structurally valid key=value should fail if key is not in allowlist."""
with self.assertRaises(ValueError):
OperationContext(user_agent_context="name=john;password=secret123")

def test_reject_invalid_app_format(self):
with self.assertRaises(ValueError):
OperationContext(user_agent_context="app=noslash")


class TestOperationContextConfig(unittest.TestCase):
"""Tests for operation_context on DataverseConfig."""
Expand Down
Loading