Add Anthropic access-review driver and resolver

The driver lists organization members through the Anthropic Admin API
(GET /v1/organizations/users) with cursor pagination, mapping the role
and the RFC 3339 added_at timestamp. The name resolver reads the
organization name from /v1/organizations/me; a non-2xx response (for
example a revoked key) yields no name rather than making the
source-name worker retry forever. Both send the required
anthropic-version header.

Add a VCR test helper that injects the key via x-api-key so the
cassette stays recordable, and strip x-api-key on save. The cassette
holds synthetic members covering the user, developer, and admin roles.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-05-28 22:03:51 +02:00
parent f6510e25a1
commit 50093c9d19
5 changed files with 362 additions and 0 deletions

View File

@@ -437,6 +437,55 @@ func (r *openaiNameResolver) ResolveInstanceName(ctx context.Context) (string, e
return resp.Name, nil
}
// anthropicNameResolver resolves the Anthropic organization name via the
// Admin API /v1/organizations/me endpoint, which returns the org an
// admin key belongs to.
type anthropicNameResolver struct {
httpClient *http.Client
}
func NewAnthropicNameResolver(httpClient *http.Client) NameResolver {
return &anthropicNameResolver{httpClient: httpClient}
}
func (r *anthropicNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://api.anthropic.com/v1/organizations/me",
nil,
)
if err != nil {
return "", fmt.Errorf("cannot create anthropic organization request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("anthropic-version", anthropicAPIVersion)
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute anthropic organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
// Best-effort: a non-2xx (e.g. a revoked admin key) must not make the
// source-name worker retry forever. Give up gracefully and keep the
// generic source name; a dead key surfaces on the next ListAccounts.
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return "", nil
}
var resp struct {
Name string `json:"name"`
}
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("cannot decode anthropic organization response: %w", err)
}
return resp.Name, nil
}
// sentryNameResolver resolves the Sentry organization name.
type sentryNameResolver struct {
httpClient *http.Client