Add Okta users driver and name resolver

The driver lists GET /api/v1/users (limit=200) on the customer's org
host and follows the RFC 5988 Link header, pinning pagination to the
configured host so a response cannot redirect the crawl off-tenant.
User status maps to the three-valued Active flag (SUSPENDED and
DEPROVISIONED are inactive); ExternalID is the stable Okta user id.

The name resolver reads /api/v1/org and returns ("", nil) on any
non-2xx so a read-only token lacking org-settings read does not loop
the source-name worker.

The org domain is operator-supplied and feeds the URL host, so it is
the one SSRF-sensitive input: NormalizeOktaDomain validates and
strips it on the write path and IsValidOktaDomain re-checks it at
driver construction, on top of the transport's SSRF protection.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-06-04 14:45:01 +02:00
parent 511472aca3
commit 06603372d7
6 changed files with 594 additions and 0 deletions

View File

@@ -898,6 +898,59 @@ func (r *datadogNameResolver) ResolveInstanceName(_ context.Context) (string, er
return r.region, nil
}
// oktaNameResolver resolves the Okta org name via GET /api/v1/org on the
// configured org host. A non-2xx is terminal — a read-only API token may
// lack org-settings read, so it returns ("", nil) to keep the generic
// source name rather than make the source-name worker retry forever.
type oktaNameResolver struct {
httpClient *http.Client
domain string
}
func NewOktaNameResolver(httpClient *http.Client, domain string) NameResolver {
return &oktaNameResolver{httpClient: httpClient, domain: domain}
}
func (r *oktaNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
if r.domain == "" {
return "", nil
}
endpoint := url.URL{Scheme: "https", Host: r.domain, Path: "/api/v1/org"}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return "", fmt.Errorf("cannot create okta org request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute okta org request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return "", nil
}
var resp struct {
CompanyName string `json:"companyName"`
Subdomain string `json:"subdomain"`
}
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("cannot decode okta org response: %w", err)
}
if resp.CompanyName != "" {
return resp.CompanyName, nil
}
return resp.Subdomain, nil
}
// asanaNameResolver resolves the Asana workspace name.
type asanaNameResolver struct {
httpClient *http.Client