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

@@ -185,16 +185,28 @@ type Options struct {
CACertPath string
// Token is an optional pre-existing bearer token.
Token string
// ServiceName is the name of this service as registered in MCIAS. It is
// sent with every Login call so MCIAS can evaluate service-context policy
// rules (e.g. deny guest users from logging into this service).
// Populate from [mcias] service_name in the service's config file.
ServiceName string
// Tags are the service-level tags sent with every Login call. MCIAS
// evaluates auth:login policy against these tags, enabling rules such as
// "deny guest accounts from services tagged env:restricted".
// Populate from [mcias] tags in the service's config file.
Tags []string
}
// Client is a thread-safe MCIAS REST API client.
// Security: the bearer token is guarded by a sync.RWMutex; it is never
// written to logs or included in error messages in this library.
type Client struct {
baseURL string
http *http.Client
mu sync.RWMutex
token string
baseURL string
http *http.Client
serviceName string
tags []string
mu sync.RWMutex
token string
}
// ---------------------------------------------------------------------------
@@ -224,9 +236,11 @@ func New(serverURL string, opts Options) (*Client, error) {
}
transport := &http.Transport{TLSClientConfig: tlsCfg}
c := &Client{
baseURL: serverURL,
http: &http.Client{Transport: transport},
token: opts.Token,
baseURL: serverURL,
http: &http.Client{Transport: transport},
token: opts.Token,
serviceName: opts.ServiceName,
tags: opts.Tags,
}
return c, nil
}
@@ -343,16 +357,28 @@ func (c *Client) GetPublicKey() (*PublicKey, error) {
// Login authenticates with username and password. On success the token is
// stored in the Client and returned along with the expiry timestamp.
// totpCode may be empty for accounts without TOTP.
//
// The client's ServiceName and Tags (from Options) are included in the
// request so MCIAS can evaluate service-context policy rules.
func (c *Client) Login(username, password, totpCode string) (token, expiresAt string, err error) {
req := map[string]string{"username": username, "password": password}
body := map[string]interface{}{
"username": username,
"password": password,
}
if totpCode != "" {
req["totp_code"] = totpCode
body["totp_code"] = totpCode
}
if c.serviceName != "" {
body["service_name"] = c.serviceName
}
if len(c.tags) > 0 {
body["tags"] = c.tags
}
var resp struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
}
if err := c.do(http.MethodPost, "/v1/auth/login", req, &resp); err != nil {
if err := c.do(http.MethodPost, "/v1/auth/login", body, &resp); err != nil {
return "", "", err
}
c.setToken(resp.Token)