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:
@@ -436,6 +436,12 @@ type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TOTPCode string `json:"totp_code,omitempty"`
|
||||
// ServiceName and Tags identify the calling service. MCIAS evaluates the
|
||||
// auth:login policy with these as the resource context, enabling operators
|
||||
// to restrict which roles/account-types may log into specific services.
|
||||
// Clients populate these from their [mcias] config section.
|
||||
ServiceName string `json:"service_name,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// loginResponse is the response body for a successful login.
|
||||
@@ -546,13 +552,42 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// Login succeeded: clear any outstanding failure counter.
|
||||
_ = s.db.ClearLoginFailures(acct.ID)
|
||||
|
||||
// Determine expiry.
|
||||
expiry := s.cfg.DefaultExpiry()
|
||||
// Load roles for expiry decision and policy check.
|
||||
roles, err := s.db.GetRoles(acct.ID)
|
||||
if err != nil {
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
// Policy check: evaluate auth:login with the calling service's context.
|
||||
// Operator rules can deny login based on role, account type, service name,
|
||||
// or tags. The built-in default Allow for auth:login is overridden by any
|
||||
// matching Deny rule (deny-wins semantics).
|
||||
//
|
||||
// Security: policy is checked after credential verification so that a
|
||||
// policy-denied login returns 403 (not 401), distinguishing a service
|
||||
// access restriction from a wrong password without leaking user existence.
|
||||
{
|
||||
input := policy.PolicyInput{
|
||||
Subject: acct.UUID,
|
||||
AccountType: string(acct.AccountType),
|
||||
Roles: roles,
|
||||
Action: policy.ActionLogin,
|
||||
Resource: policy.Resource{
|
||||
ServiceName: req.ServiceName,
|
||||
Tags: req.Tags,
|
||||
},
|
||||
}
|
||||
if effect, _ := s.polEng.Evaluate(input); effect == policy.Deny {
|
||||
s.writeAudit(r, model.EventLoginFail, &acct.ID, nil,
|
||||
audit.JSON("reason", "policy_deny", "service_name", req.ServiceName))
|
||||
middleware.WriteError(w, http.StatusForbidden, "access denied by policy", "policy_denied")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Determine expiry.
|
||||
expiry := s.cfg.DefaultExpiry()
|
||||
for _, r := range roles {
|
||||
if r == "admin" {
|
||||
expiry = s.cfg.AdminExpiry()
|
||||
|
||||
Reference in New Issue
Block a user