package proxmox import ( "fmt" "regexp" ) // permRe extracts the offending privilege (and path) from a Proxmox permission // message, e.g. "Permission check failed (/vms/9000, VM.Backup)" or // "403 Permission check failed (/sdn/zones/localnetwork/vmbr0, SDN.Use)". var permRe = regexp.MustCompile(`Permission check failed \(([^,]+),\s*([^)]+)\)`) // APIError is returned for a non-2xx HTTP response from the Proxmox API. On a 403 // it parses the offending path + privilege so a role misconfiguration is // diagnosable (the FelhomAgent role is exactly 16 privileges — see doc.go). type APIError struct { StatusCode int Method string Path string // request path Body string // response body (trimmed) // Populated from a permission-check message when present: DeniedPath string // ACL path, e.g. "/vms/9000" Privilege string // e.g. "VM.Backup" } func (e *APIError) Error() string { if e.Privilege != "" { return fmt.Sprintf("proxmox: %s %s -> HTTP %d: permission denied at %s (missing privilege %s)", e.Method, e.Path, e.StatusCode, e.DeniedPath, e.Privilege) } return fmt.Sprintf("proxmox: %s %s -> HTTP %d: %s", e.Method, e.Path, e.StatusCode, e.Body) } // IsForbidden reports whether this was an HTTP 403. func (e *APIError) IsForbidden() bool { return e.StatusCode == 403 } // newAPIError builds an APIError, extracting privilege info from the body. func newAPIError(statusCode int, method, path, body string) *APIError { e := &APIError{StatusCode: statusCode, Method: method, Path: path, Body: trimBody(body)} if m := permRe.FindStringSubmatch(body); m != nil { e.DeniedPath = m[1] e.Privilege = m[2] } return e } // TaskError is returned by WaitTask when a task stops with a non-OK exitstatus. // The authorization failure for a mutating op surfaces here (in the task // exitstatus), not at the HTTP POST — so callers must always WaitTask. type TaskError struct { UPID string ExitStatus string // e.g. "403 Permission check failed (/vms/9000, VM.Backup)" LogTail []string // last lines of the task log, for diagnosis DeniedPath string Privilege string } func (e *TaskError) Error() string { if e.Privilege != "" { return fmt.Sprintf("proxmox: task %s failed: permission denied at %s (missing privilege %s)", e.UPID, e.DeniedPath, e.Privilege) } return fmt.Sprintf("proxmox: task %s failed: exitstatus %q", e.UPID, e.ExitStatus) } func newTaskError(upid, exitStatus string, logTail []string) *TaskError { e := &TaskError{UPID: upid, ExitStatus: exitStatus, LogTail: logTail} if m := permRe.FindStringSubmatch(exitStatus); m != nil { e.DeniedPath = m[1] e.Privilege = m[2] } return e } func trimBody(s string) string { const max = 512 if len(s) > max { return s[:max] + "…" } return s }