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

@@ -227,6 +227,10 @@ struct LoginRequest<'a> {
password: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
totp_code: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
service_name: Option<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
}
#[derive(Deserialize)]
@@ -268,6 +272,16 @@ pub struct ClientOptions {
/// Optional pre-existing bearer token.
pub token: Option<String>,
/// This service's name as registered in MCIAS. Sent with every login
/// request so MCIAS can evaluate service-context policy rules.
/// Populate from `[mcias] service_name` in the service config.
pub service_name: Option<String>,
/// Service-level tags sent with every login request. MCIAS evaluates
/// `auth:login` policy against these tags.
/// Populate from `[mcias] tags` in the service config.
pub tags: Vec<String>,
}
// ---- Client ----
@@ -280,6 +294,8 @@ pub struct ClientOptions {
pub struct Client {
base_url: String,
http: reqwest::Client,
service_name: Option<String>,
tags: Vec<String>,
/// Bearer token storage. `Arc<RwLock<...>>` so clones share the token.
/// Security: the token is never logged or included in error messages.
token: Arc<RwLock<Option<String>>>,
@@ -306,6 +322,8 @@ impl Client {
Ok(Self {
base_url: base_url.trim_end_matches('/').to_owned(),
http,
service_name: opts.service_name,
tags: opts.tags,
token: Arc::new(RwLock::new(opts.token)),
})
}
@@ -336,6 +354,8 @@ impl Client {
username,
password,
totp_code,
service_name: self.service_name.as_deref(),
tags: self.tags.clone(),
};
let resp: TokenResponse = self.post("/v1/auth/login", &body).await?;
*self.token.write().await = Some(resp.token.clone());