Add missing resources to CLI, MCP, and n8n surfaces
Audit all three API surfaces against the console GraphQL schema and add missing resources: asset, audit, datum, dpia, evidence upload, measure, obligation, processing activity, rights request, snapshot, task, tia, trust center (with references/files), and vendor management CLI commands; MCP tools for deletes, rights requests, trust center, vendor contacts/services, and compliance external URLs; n8n nodes for obligation, finding, task, evidence, processing activity, dpia, tia, rights request, snapshot, audit log, access review, organization context, trust center, and additional control/measure/vendor operations. Include MCP e2e test infrastructure (testutil MCP client with API key auth and JSON-RPC session management) and tests covering all new MCP tools. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -193,6 +194,134 @@ func (c *Client) doRequest(
|
||||
return respBody, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
// DoUpload sends a GraphQL multipart file upload request following the
|
||||
// graphql-multipart-request-spec. It maps the given file to the variable
|
||||
// at the provided path (e.g. "variables.input.file").
|
||||
func (c *Client) DoUpload(
|
||||
query string,
|
||||
variables map[string]any,
|
||||
varPath string,
|
||||
filename string,
|
||||
file io.Reader,
|
||||
) (json.RawMessage, error) {
|
||||
raw, err := c.doUploadRequest(query, variables, varPath, filename, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp graphQLResponse
|
||||
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse GraphQL response: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Errors) > 0 {
|
||||
var msg strings.Builder
|
||||
msg.WriteString(resp.Errors[0].Message)
|
||||
for _, e := range resp.Errors[1:] {
|
||||
msg.WriteString("; " + e.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("GraphQL error: %s", msg.String())
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
func (c *Client) doUploadRequest(
|
||||
query string,
|
||||
variables map[string]any,
|
||||
varPath string,
|
||||
filename string,
|
||||
file io.Reader,
|
||||
) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
|
||||
// Part 1: operations
|
||||
operationsJSON, err := json.Marshal(graphQLRequest{
|
||||
Query: query,
|
||||
Variables: variables,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal operations: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.WriteField("operations", string(operationsJSON)); err != nil {
|
||||
return nil, fmt.Errorf("cannot write operations field: %w", err)
|
||||
}
|
||||
|
||||
// Part 2: map
|
||||
mapJSON, err := json.Marshal(map[string][]string{
|
||||
"0": {varPath},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal map: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.WriteField("map", string(mapJSON)); err != nil {
|
||||
return nil, fmt.Errorf("cannot write map field: %w", err)
|
||||
}
|
||||
|
||||
// Part 3: file
|
||||
part, err := writer.CreateFormFile("0", filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create form file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(part, file); err != nil {
|
||||
return nil, fmt.Errorf("cannot write file content: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("cannot close multipart writer: %w", err)
|
||||
}
|
||||
|
||||
host := c.host
|
||||
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||
host = "https://" + host
|
||||
}
|
||||
|
||||
reqURL := host + c.endpoint
|
||||
req, err := http.NewRequest(http.MethodPost, reqURL, &buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create HTTP request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("User-Agent", version.UserAgent("prb"))
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot send HTTP request: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read HTTP response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized && c.refresher != nil {
|
||||
if refreshErr := c.tryRefreshToken(); refreshErr == nil {
|
||||
// Retry — but we can't re-read the file, so return the original error.
|
||||
return nil, fmt.Errorf("authentication failed (HTTP 401): token was refreshed, please retry the command")
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
switch resp.StatusCode {
|
||||
case http.StatusUnauthorized:
|
||||
return nil, fmt.Errorf("authentication failed (HTTP 401): token may be invalid or expired, try 'prb auth login'")
|
||||
case http.StatusForbidden:
|
||||
return nil, fmt.Errorf("access denied (HTTP 403): you do not have permission to perform this action")
|
||||
default:
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
func (c *Client) tryRefreshToken() error {
|
||||
r := c.refresher
|
||||
|
||||
|
||||
Reference in New Issue
Block a user