Skip to content

feat: support external_id in identity commands#444

Merged
alnr merged 4 commits into
masterfrom
alnr/identity-external-id
Jul 9, 2026
Merged

feat: support external_id in identity commands#444
alnr merged 4 commits into
masterfrom
alnr/identity-external-id

Conversation

@alnr

@alnr alnr commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Related Issue or Design Document

Closes #413

The external_id field is part of the Ory identity API but was not usable from
the CLI: ory identity import did not surface it and there was no way to look
an identity up by its external ID.

The Ory CLI identity commands wrap Ory Kratos'
CLI commands, so the feature itself landed in Kratos. This PR pulls it in and
exposes it:

  • ory get identity --external-id <externalID> looks an identity up by its
    external ID (via GET /admin/identities/by/external/{externalID}).
  • The identity get/list/import output now includes an EXTERNAL ID
    column.
  • import continues to forward the external_id field.

Changes

  • Bump github.com/ory/kratos to the master version that adds external_id
    support to the identity CLI commands, and bump github.com/ory/x accordingly
    (Kratos now requires corsx.ClassifyOrigin).
  • fix(client): attach the Ory-RateLimit-Action header
    (ORY_RATE_LIMIT_HEADER) to the project HTTP client as well, not just the Ory
    Network SDK client. Previously the admin API calls made by the wrapped Kratos
    and Hydra CLI commands were not covered by the rate-limit bypass header during
    E2E tests. No-op unless the env var is set.
  • Add an E2E test that imports an identity with an external_id and looks it up
    via ory get identity --external-id, asserting the round trip against the Ory
    Network (plus the not-found path).

Checklist

  • I have read the contributing guidelines
    and signed the CLA.
  • I have referenced an issue containing the design document if my change
    introduces a new feature.
  • I have read the security policy.
  • I confirm that this pull request does not address a security
    vulnerability.
  • I have added tests that prove my fix is effective or that my feature
    works.
  • I have added the necessary documentation within the code base (if
    appropriate).

Further comments

The external_id column is VARCHAR(64); the E2E test keeps generated external
IDs well under that limit.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved request handling so certain API calls now include the appropriate rate-limit header automatically.
    • Added support for looking up identities by external ID in the CLI.
  • Bug Fixes

    • Fixed identity retrieval behavior for external IDs, including clearer handling when no matching identity exists.
  • Tests

    • Added coverage for importing identities and verifying external ID lookup behavior.

alnr and others added 3 commits July 9, 2026 15:13
Bump github.com/ory/kratos to the master version that adds external_id
support to the identity CLI commands (which the Ory CLI wraps), and bump
github.com/ory/x accordingly (kratos now requires corsx.ClassifyOrigin).

This exposes, with no further wiring:
- get identity --external-id <externalID> to look up by external ID
- an EXTERNAL ID column in identity get/list/import output

Closes #413

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ORY_RATE_LIMIT_HEADER value was only attached to the Ory Network SDK
client, not to the project HTTP client used by the wrapped Ory Kratos and
Ory Hydra admin CLI commands (identity import/get/list, etc.). As a result
those admin API calls were still subject to rate limiting during E2E tests.

Attach the header to the project HTTP client too. No-op unless the env var
is set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Import an identity with an external_id and look it up via
'ory get identity --external-id', asserting the round trip against the
Ory Network. Also covers the not-found path. Runs against staging with
ORY_RATE_LIMIT_HEADER set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@alnr, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 34472641-01be-4586-a3a4-147af23b98d3

📥 Commits

Reviewing files that changed from the base of the PR and between e269620 and ed26675.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • .github/workflows/ci.yaml
  • cmd/cloudx/client/command_helper_test.go
  • cmd/cloudx/testhelpers/testhelpers.go
  • go.mod
📝 Walkthrough

Walkthrough

Adds a setHeaderTransport HTTP RoundTripper that injects a fixed header into outgoing requests, wires it into the project HTTP client to set a rate-limit header when configured, adds an integration test for retrieving identities by external_id, and updates several go.mod dependency versions.

Changes

Rate-limit header transport

Layer / File(s) Summary
Header-injecting transport and wiring
cmd/cloudx/client/http_client.go, cmd/cloudx/client/sdks.go
New setHeaderTransport clones requests and sets a configured header before delegating to a base transport; newProjectHTTPClient wraps its transport with it to inject Ory-RateLimit-Action when rateLimitHeader is non-empty.

External ID lookup test

Layer / File(s) Summary
Import and lookup by external_id test
cmd/cloudx/identity/get_external_id_test.go
New TestGetIdentityByExternalID imports an identity with a generated external_id, then asserts CLI get identity --external-id returns the matching identity and errors for an unknown external_id.

Dependency version updates

Layer / File(s) Summary
go.mod dependency bumps and removals
go.mod
Updates versions for ory/kratos, ory/x, adds connectrpc.com/connect, refreshes gorilla/grpc-ecosystem/hashicorp/jackc/jaeger-idl indirect group, bumps prometheus/common, OpenTelemetry, genproto, and modernc.org/sqlite, and removes mailhog, smarty/assertions, t-k/fluent-logger-golang, and gopkg.in/mgo.v2.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client as ProjectHTTPClient
  participant SHT as setHeaderTransport
  participant Base as BaseTransport
  participant API as ProjectAPI
  Client->>SHT: RoundTrip(request)
  SHT->>SHT: clone request, set Ory-RateLimit-Action header
  SHT->>Base: RoundTrip(clonedRequest)
  Base->>API: send request with header
  API-->>Client: response
Loading

Suggested reviewers: zepatrik

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding external_id support to identity commands.
Linked Issues check ✅ Passed The PR adds external_id import/list/get support and an E2E test covering import and lookup, matching #413's requirements.
Out of Scope Changes check ✅ Passed The dependency bumps and HTTP header tweak are part of the stated objective, so no clear out-of-scope changes stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alnr/identity-external-id

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
cmd/cloudx/client/sdks.go (1)

128-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the hardcoded header name into a constant.

The string "Ory-RateLimit-Action" is duplicated here and at line 61 in newSDKConfiguration. Extracting it into a shared constant (e.g., rateLimitActionHeader) alongside the existing RateLimitHeaderKey prevents drift if the header name changes.

♻️ Suggested refactor
+const rateLimitActionHeader = "Ory-RateLimit-Action"
+
 // ... in newSDKConfiguration:
-	if rateLimitHeader != "" {
-		conf.AddDefaultHeader("Ory-RateLimit-Action", rateLimitHeader)
-	}
+	if rateLimitHeader != "" {
+		conf.AddDefaultHeader(rateLimitActionHeader, rateLimitHeader)
+	}

 // ... in newProjectHTTPClient:
 	if rateLimitHeader != "" {
-		c.Transport = &setHeaderTransport{base: c.Transport, key: "Ory-RateLimit-Action", value: rateLimitHeader}
+		c.Transport = &setHeaderTransport{base: c.Transport, key: rateLimitActionHeader, value: rateLimitHeader}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/cloudx/client/sdks.go` around lines 128 - 130, The header name is
hardcoded in both the transport setup and newSDKConfiguration, so extract it
into a shared constant. Add a single constant for the Ory rate limit action
header near the existing RateLimitHeaderKey definition, then update
setHeaderTransport usage in the client setup to reference that constant instead
of the literal string.
cmd/cloudx/identity/get_external_id_test.go (1)

27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using encoding/json for JSON construction.

The import JSON is built via string concatenation with email. If testhelpers.FakeEmail() ever returns a value containing " or \, the JSON would be malformed and the test would fail with a confusing error. Using encoding/json.Marshal would eliminate this risk entirely.

This is low-risk given typical fake email generators produce safe characters, so it's purely a defensive improvement.

♻️ Optional refactor: use encoding/json
 import (
 	"os"
 	"path/filepath"
 	"testing"
+	"encoding/json"

 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tidwall/gjson"

 	"github.com/ory/cli/cmd/cloudx/testhelpers"
 	"github.com/ory/x/randx"
 )
+
+type importPayload struct {
+	SchemaID   string            `json:"schema_id"`
+	Traits     map[string]string `json:"traits"`
+	ExternalID string            `json:"external_id"`
+}

Then in the test body:

-	require.NoError(t, os.WriteFile(importPath, []byte(`{
-  "schema_id": "preset://username",
-  "traits": {
-    "username": "`+email+`"
-  },
-  "external_id": "`+externalID+`"
-}`), 0o600))
+	payload, err := json.Marshal(importPayload{
+		SchemaID:   "preset://username",
+		Traits:    map[string]string{"username": email},
+		ExternalID: externalID,
+	})
+	require.NoError(t, err)
+	require.NoError(t, os.WriteFile(importPath, payload, 0o600))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/cloudx/identity/get_external_id_test.go` around lines 27 - 33, The test
data in getExternalID is building JSON by string concatenation, which is fragile
if FakeEmail() ever returns characters that need escaping. Update the import
payload construction in getExternalID to use encoding/json marshalling instead
of manual string assembly, so the JSON written by os.WriteFile is always valid.
Keep the same fields (schema_id, traits.username, external_id) and replace the
inline literal construction with a marshaled struct/map near the existing
importPath setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmd/cloudx/client/sdks.go`:
- Around line 128-130: The header name is hardcoded in both the transport setup
and newSDKConfiguration, so extract it into a shared constant. Add a single
constant for the Ory rate limit action header near the existing
RateLimitHeaderKey definition, then update setHeaderTransport usage in the
client setup to reference that constant instead of the literal string.

In `@cmd/cloudx/identity/get_external_id_test.go`:
- Around line 27-33: The test data in getExternalID is building JSON by string
concatenation, which is fragile if FakeEmail() ever returns characters that need
escaping. Update the import payload construction in getExternalID to use
encoding/json marshalling instead of manual string assembly, so the JSON written
by os.WriteFile is always valid. Keep the same fields (schema_id,
traits.username, external_id) and replace the inline literal construction with a
marshaled struct/map near the existing importPath setup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a8ed5bc2-cff0-42c7-8582-c7402e09e0ae

📥 Commits

Reviewing files that changed from the base of the PR and between b73c06e and e269620.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • cmd/cloudx/client/http_client.go
  • cmd/cloudx/client/sdks.go
  • cmd/cloudx/identity/get_external_id_test.go
  • go.mod

playwright-community/playwright-go v0.4702.0 downloads its driver from
the retired playwright.azureedge.net CDN, which now 404s and breaks the
'Run tests' CI job before any test runs.

Upgrade to github.com/mxschmitt/playwright-go v0.6100.0 (the module moved
back to that path), which fetches the driver via npm and downloads
browsers from cdn.playwright.dev instead of azureedge. Update the two
imports and the CI install command accordingly.

Verified locally: driver + chromium install succeeds and the identity
external-id E2E test passes against staging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alnr alnr self-assigned this Jul 9, 2026
@alnr alnr merged commit afd84ac into master Jul 9, 2026
23 of 25 checks passed
@alnr alnr deleted the alnr/identity-external-id branch July 9, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

external_id doesn't work for import or list of identities

2 participants