Add service-context login policy enforcement

Services send service_name and tags in POST /v1/auth/login.
MCIAS evaluates auth:login policy with these as the resource
context after credentials are verified, enabling rules like:
  deny guest/viewer human accounts from env:restricted services
  deny guest accounts from specific named services

- loginRequest: add ServiceName and Tags fields
- handleLogin: evaluate policy after credential+TOTP check;
  policy deny returns 403 (not 401) to distinguish access
  restriction from bad credentials
- Go client: Options.ServiceName/Tags stored on Client,
  sent automatically in every Login() call
- Python client: service_name/tags on __init__, sent in login()
- Rust client: ClientOptions.service_name/tags, LoginRequest
  fields, Client stores and sends them in login()
- openapi.yaml: document service_name/tags request fields
  and 403 response for policy-denied logins
- engineering-standards.md: document service_name/tags in
  [mcias] config section with policy examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 21:09:38 -07:00
parent b0afe3b993
commit 39d9ffb79a
5 changed files with 136 additions and 12 deletions

View File

@@ -20,9 +20,13 @@ class Client:
ca_cert_path: str | None = None,
token: str | None = None,
timeout: float = 30.0,
service_name: str | None = None,
tags: list[str] | None = None,
) -> None:
self._base_url = server_url.rstrip("/")
self.token = token
self._service_name = service_name
self._tags = tags or []
ssl_context: ssl.SSLContext | bool
if ca_cert_path is not None:
ssl_context = ssl.create_default_context(cafile=ca_cert_path)
@@ -115,6 +119,9 @@ class Client:
) -> tuple[str, str]:
"""POST /v1/auth/login — authenticate and obtain a JWT.
Returns (token, expires_at). Stores the token on self.token.
The client's service_name and tags are included so MCIAS can evaluate
service-context policy rules (e.g. deny guests from restricted services).
"""
payload: dict[str, Any] = {
"username": username,
@@ -122,6 +129,10 @@ class Client:
}
if totp_code is not None:
payload["totp_code"] = totp_code
if self._service_name is not None:
payload["service_name"] = self._service_name
if self._tags:
payload["tags"] = self._tags
data = self._request("POST", "/v1/auth/login", json=payload)
assert data is not None
token = str(data["token"])