package web import ( "encoding/json" "net/http" ) // SSODetector maps an incoming HTTP request from an already-authenticated // portal session into a YAMA web identity (username + role). Implementations // are portal-specific: each one knows how to recognize its portal's session // credential (a cookie, a header, a JWT, ...) and translate the caller to // YAMA's two roles, "admin" and "viewer". // // Detect returns ok=false when the request carries no usable credential, or // the credential is invalid/expired; the caller then falls back to the // built-in login form. username is used only for audit/logging. Whether a // detected identity is actually bridged into a session is handleSSO's policy: // currently only "admin" is bridged, everyone else falls back to the login form. type SSODetector interface { Detect(r *http.Request) (username, role string, ok bool) } // handleSSO bridges a detected portal identity into a YAMA session token. // Only administrators are bridged: a valid portal session for any other role // (viewer, unknown) still returns 401 and the front-end falls back to the // built-in login form, so a low-privileged portal account cannot walk straight // into the remote-desktop console. When no detector is configured, or the // request is not recognized, it also returns 401. func (s *Server) handleSSO(w http.ResponseWriter, r *http.Request) { if s.sso == nil { s.unauthorized(w) return } username, role, ok := s.sso.Detect(r) if !ok { // No usable portal credential: the normal fallback to the login form. // Not audited — portal users without YAMA access hit this routinely. s.unauthorized(w) return } // Only the "admin" role may skip the login form via SSO. Every other role // (viewer, unknown) must authenticate against YAMA's own login, even with // a valid portal session. A valid portal identity being refused here is // worth an audit record: it is an authorization decision, not a login-form // fallback. if role != "admin" { fields := requestFields(r, s.trustForwardedFor) fields["method"] = "sso" fields["actor"] = username fields["role"] = role fields["reason"] = "role_not_allowed" s.log.AuditWarn(auditLoginFail, fields) s.unauthorized(w) return } token, err := s.auth.IssueToken(username, role) if err != nil { fields := requestFields(r, s.trustForwardedFor) fields["method"] = "sso" fields["actor"] = username fields["role"] = role fields["reason"] = "token_issue_failed" fields["error"] = err.Error() s.log.AuditWarn(auditLoginFail, fields) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"internal error"}`)) return } fields := requestFields(r, s.trustForwardedFor) fields["method"] = "sso" fields["actor"] = username fields["role"] = role s.log.Audit(auditLoginOK, fields) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") _ = json.NewEncoder(w).Encode(map[string]string{ "token": token, "role": role, "username": username, }) }