-
Notifications
You must be signed in to change notification settings - Fork 61
Fix keychain credential :external_id bug
#4177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,9 @@ defmodule Lightning.Credentials.Credential do | |
| |> unique_constraint([:name, :user_id], | ||
| message: "you have another credential with the same name" | ||
| ) | ||
| |> unique_constraint([:external_id, :user_id], | ||
| message: "you have another credential with the external ID" | ||
| ) | ||
|
Comment on lines
+53
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This constraint enforces uniqueness per-user, but the bug we're fixing requires uniqueness per-project, right ? Also, this needs a corresponding database index/migration to actually work. I scanned a little bit the migrations and the changes in this PR but I haven't seen one. Without a db migration for the constraint, this probably won't work. |
||
| |> assoc_constraint(:user) | ||
| |> assoc_constraint(:oauth_client) | ||
| |> validate_format(:name, ~r/^[a-zA-Z0-9_\- ]*$/, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -411,4 +411,72 @@ defmodule Lightning.Credentials.KeychainCredentialTest do | |
| assert loaded_keychain.default_credential.id == credential.id | ||
| end | ||
| end | ||
|
|
||
| describe "external_id uniqueness validation" do | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice test documenting the failure mode for legacy data! But this makes me think, users in prod might already have duplicates in their database, right? They'd hit this |
||
| test "keychain resolver raises error when duplicate external_ids exist in same project" do | ||
| # This test documents the behavior when duplicate external_ids exist | ||
| # (which should be prevented by validation, but could occur from legacy data) | ||
| user = insert(:user) | ||
| project = insert(:project, project_users: [%{user: user}]) | ||
|
|
||
| # Create credentials directly with insert to bypass validation | ||
| # (simulating legacy data or direct DB manipulation) | ||
| credential_a = | ||
| insert(:credential, | ||
| name: "Credential A", | ||
| schema: "raw", | ||
| external_id: "duplicate-id", | ||
| user: user | ||
| ) | ||
| |> with_body(%{name: "main", body: %{"key" => "value_a"}}) | ||
|
|
||
| credential_b = | ||
| insert(:credential, | ||
| name: "Credential B", | ||
| schema: "raw", | ||
| external_id: "duplicate-id", | ||
| user: user | ||
| ) | ||
| |> with_body(%{name: "main", body: %{"key" => "value_b"}}) | ||
|
|
||
| # Associate both credentials with the same project | ||
| insert(:project_credential, project: project, credential: credential_a) | ||
| insert(:project_credential, project: project, credential: credential_b) | ||
|
|
||
| # Create a keychain credential | ||
| keychain_credential = | ||
| insert(:keychain_credential, | ||
| name: "Test Keychain", | ||
| path: "$.user_id", | ||
| project: project, | ||
| created_by: user | ||
| ) | ||
|
|
||
| # Create workflow with the keychain credential | ||
| %{jobs: [job]} = | ||
| workflow = | ||
| build(:workflow, project: project) | ||
| |> with_job(%{keychain_credential: keychain_credential}) | ||
| |> insert() | ||
|
|
||
| # Create a run with dataclip that matches the duplicate external_id | ||
| %{runs: [run]} = | ||
| insert(:workorder, workflow: workflow) | ||
| |> with_run(%{ | ||
| dataclip: | ||
| build(:dataclip, %{ | ||
| body: %{"user_id" => "duplicate-id"} | ||
| }), | ||
| starting_job: job | ||
| }) | ||
|
|
||
| # The resolver should raise an error because Repo.one() finds multiple results | ||
| assert_raise Ecto.MultipleResultsError, fn -> | ||
| Lightning.Credentials.Resolver.resolve_credential( | ||
| run, | ||
| keychain_credential.id | ||
| ) | ||
| end | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This validation here runs outside the multi and transaction. This means there are high chances of a race condition. In fact, two concurrent requests could both pass validation before either inserts. This flow for example:
Now this may be very rare situation but it can happen, and when it happens it will break things.
We should move this validation inside the Multi so the check and insert happen in the same transaction.