From 09aa12f8f2e1dac8289b5ed7417e23854e2675c4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 28 Mar 2024 15:40:23 -0500 Subject: [PATCH 1/4] chore: enforce unique linked_ids Duplicate linked_ids make no sense. 2 users cannot share the same source user from a provider --- coderd/database/dump.sql | 2 ++ .../000205_unique_linked_id.down.sql | 1 + .../migrations/000205_unique_linked_id.up.sql | 18 ++++++++++++++++++ .../testdata/fixtures/000048_userdelete.up.sql | 15 +++++++++++++++ coderd/database/unique_constraint.go | 1 + 5 files changed, 37 insertions(+) create mode 100644 coderd/database/migrations/000205_unique_linked_id.down.sql create mode 100644 coderd/database/migrations/000205_unique_linked_id.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 55872db31d55a..24ec8a9bc3505 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1624,6 +1624,8 @@ CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coo CREATE INDEX idx_tailnet_peers_coordinator ON tailnet_peers USING btree (coordinator_id); +CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id <> ''::text); + CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); diff --git a/coderd/database/migrations/000205_unique_linked_id.down.sql b/coderd/database/migrations/000205_unique_linked_id.down.sql new file mode 100644 index 0000000000000..100c98903bc0a --- /dev/null +++ b/coderd/database/migrations/000205_unique_linked_id.down.sql @@ -0,0 +1 @@ +DROP INDEX idx_user_link_linked_id; diff --git a/coderd/database/migrations/000205_unique_linked_id.up.sql b/coderd/database/migrations/000205_unique_linked_id.up.sql new file mode 100644 index 0000000000000..60360891afef1 --- /dev/null +++ b/coderd/database/migrations/000205_unique_linked_id.up.sql @@ -0,0 +1,18 @@ +-- Remove the linked_id if two user_links share the same value. +-- This will affect the user if they attempt to change their settings on +-- the oauth/oidc provider. However, if two users exist with the same +-- linked_value, there is no way to determine correctly which user should +-- be updated. Since the linked_id is empty, this value will be linked +-- by email. +UPDATE ONLY user_links out +SET + linked_id = + CASE WHEN ( + -- When the count of linked_id is greater than 1, set the linked_id to empty + SELECT COUNT(*) + FROM user_links inn + WHERE out.linked_id = inn.linked_id + ) > 1 THEN '' ELSE out.linked_id END; + +-- Enforce unique linked_id constraint on non-empty linked_id +CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id != ''); diff --git a/coderd/database/migrations/testdata/fixtures/000048_userdelete.up.sql b/coderd/database/migrations/testdata/fixtures/000048_userdelete.up.sql index 0fb1d0efd4aca..c4f8b2e909773 100644 --- a/coderd/database/migrations/testdata/fixtures/000048_userdelete.up.sql +++ b/coderd/database/migrations/testdata/fixtures/000048_userdelete.up.sql @@ -17,3 +17,18 @@ INSERT INTO public.user_links(user_id, login_type, linked_id, oauth_access_token -- This has happened on a production database. INSERT INTO public.user_links(user_id, login_type, linked_id, oauth_access_token) VALUES('fc1511ef-4fcf-4a3b-98a1-8df64160e35a', 'oidc', 'foo', ''); + + +-- Lastly, make 2 other users who have the same user link. +INSERT INTO public.users(id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, deleted) +VALUES ('580ed397-727d-4aaf-950a-51f89f556c24', 'dup_link_a@coder.com', 'dupe_a', '\x', '2022-11-02 13:05:21.445455+02', '2022-11-02 13:05:21.445455+02', 'active', '{}', false) ON CONFLICT DO NOTHING; +INSERT INTO public.organization_members VALUES ('580ed397-727d-4aaf-950a-51f89f556c24', 'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1', '2022-11-02 13:05:21.447595+02', '2022-11-02 13:05:21.447595+02', '{}') ON CONFLICT DO NOTHING; +INSERT INTO public.user_links(user_id, login_type, linked_id, oauth_access_token) +VALUES('580ed397-727d-4aaf-950a-51f89f556c24', 'github', '500', ''); + + +INSERT INTO public.users(id, email, username, hashed_password, created_at, updated_at, status, rbac_roles, deleted) +VALUES ('c813366b-2fde-45ae-920c-101c3ad6a1e1', 'dup_link_b@coder.com', 'dupe_b', '\x', '2022-11-02 13:05:21.445455+02', '2022-11-02 13:05:21.445455+02', 'active', '{}', false) ON CONFLICT DO NOTHING; +INSERT INTO public.organization_members VALUES ('c813366b-2fde-45ae-920c-101c3ad6a1e1', 'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1', '2022-11-02 13:05:21.447595+02', '2022-11-02 13:05:21.447595+02', '{}') ON CONFLICT DO NOTHING; +INSERT INTO public.user_links(user_id, login_type, linked_id, oauth_access_token) +VALUES('c813366b-2fde-45ae-920c-101c3ad6a1e1', 'github', '500', ''); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 52de0a50f6a3e..5f51c0babbe72 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -77,6 +77,7 @@ const ( UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); UniqueIndexProvisionerDaemonsNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_name_owner_key ON provisioner_daemons USING btree (name, lower(COALESCE((tags ->> 'owner'::text), ''::text))); + UniqueIndexUserLinkLinkedID UniqueConstraint = "idx_user_link_linked_id" // CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id <> ''::text); UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); From 6afbd4513bff608ad332ba6977ee97fb17a37009 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 28 Mar 2024 15:42:35 -0500 Subject: [PATCH 2/4] add login_type check --- coderd/database/migrations/000205_unique_linked_id.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000205_unique_linked_id.up.sql b/coderd/database/migrations/000205_unique_linked_id.up.sql index 60360891afef1..8b21c0b840e0d 100644 --- a/coderd/database/migrations/000205_unique_linked_id.up.sql +++ b/coderd/database/migrations/000205_unique_linked_id.up.sql @@ -11,8 +11,8 @@ SET -- When the count of linked_id is greater than 1, set the linked_id to empty SELECT COUNT(*) FROM user_links inn - WHERE out.linked_id = inn.linked_id + WHERE out.linked_id = inn.linked_id AND out.login_type = inn.login_type ) > 1 THEN '' ELSE out.linked_id END; -- Enforce unique linked_id constraint on non-empty linked_id -CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id != ''); +CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id != ''); From 72b223a2506fba665b78542eae738a2ae64672c0 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 28 Mar 2024 19:25:17 -0500 Subject: [PATCH 3/4] make gen --- coderd/database/dump.sql | 2 +- coderd/database/unique_constraint.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 24ec8a9bc3505..2a0ab3fe36429 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1624,7 +1624,7 @@ CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coo CREATE INDEX idx_tailnet_peers_coordinator ON tailnet_peers USING btree (coordinator_id); -CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id <> ''::text); +CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 5f51c0babbe72..17896dc014bc1 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -77,7 +77,7 @@ const ( UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); UniqueIndexProvisionerDaemonsNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_name_owner_key ON provisioner_daemons USING btree (name, lower(COALESCE((tags ->> 'owner'::text), ''::text))); - UniqueIndexUserLinkLinkedID UniqueConstraint = "idx_user_link_linked_id" // CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id) WHERE (linked_id <> ''::text); + UniqueIndexUserLinkLinkedID UniqueConstraint = "idx_user_link_linked_id" // CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); From 1d54a52e5b952df465cc05a36d7790571bd571a8 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 2 Apr 2024 10:07:08 -0500 Subject: [PATCH 4/4] rename index --- coderd/database/dump.sql | 4 ++-- .../migrations/000205_unique_linked_id.down.sql | 2 +- .../migrations/000205_unique_linked_id.up.sql | 13 ++++++++----- coderd/database/unique_constraint.go | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 2a0ab3fe36429..830f8a1825b20 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1624,8 +1624,6 @@ CREATE INDEX idx_tailnet_clients_coordinator ON tailnet_clients USING btree (coo CREATE INDEX idx_tailnet_peers_coordinator ON tailnet_peers USING btree (coordinator_id); -CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); - CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); @@ -1646,6 +1644,8 @@ COMMENT ON INDEX template_usage_stats_start_time_template_id_user_id_idx IS 'Ind CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); +CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); + CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false); CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false); diff --git a/coderd/database/migrations/000205_unique_linked_id.down.sql b/coderd/database/migrations/000205_unique_linked_id.down.sql index 100c98903bc0a..81e7d14fc1aa0 100644 --- a/coderd/database/migrations/000205_unique_linked_id.down.sql +++ b/coderd/database/migrations/000205_unique_linked_id.down.sql @@ -1 +1 @@ -DROP INDEX idx_user_link_linked_id; +DROP INDEX user_links_linked_id_login_type_idx; diff --git a/coderd/database/migrations/000205_unique_linked_id.up.sql b/coderd/database/migrations/000205_unique_linked_id.up.sql index 8b21c0b840e0d..da3ff6126a113 100644 --- a/coderd/database/migrations/000205_unique_linked_id.up.sql +++ b/coderd/database/migrations/000205_unique_linked_id.up.sql @@ -4,15 +4,18 @@ -- linked_value, there is no way to determine correctly which user should -- be updated. Since the linked_id is empty, this value will be linked -- by email. -UPDATE ONLY user_links out +UPDATE ONLY user_links AS out SET linked_id = CASE WHEN ( -- When the count of linked_id is greater than 1, set the linked_id to empty - SELECT COUNT(*) - FROM user_links inn - WHERE out.linked_id = inn.linked_id AND out.login_type = inn.login_type + SELECT + COUNT(*) + FROM + user_links inn + WHERE + out.linked_id = inn.linked_id AND out.login_type = inn.login_type ) > 1 THEN '' ELSE out.linked_id END; -- Enforce unique linked_id constraint on non-empty linked_id -CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id != ''); +CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id != ''); diff --git a/coderd/database/unique_constraint.go b/coderd/database/unique_constraint.go index 17896dc014bc1..9db8af72c8cf6 100644 --- a/coderd/database/unique_constraint.go +++ b/coderd/database/unique_constraint.go @@ -77,12 +77,12 @@ const ( UniqueIndexOrganizationName UniqueConstraint = "idx_organization_name" // CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name); UniqueIndexOrganizationNameLower UniqueConstraint = "idx_organization_name_lower" // CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)); UniqueIndexProvisionerDaemonsNameOwnerKey UniqueConstraint = "idx_provisioner_daemons_name_owner_key" // CREATE UNIQUE INDEX idx_provisioner_daemons_name_owner_key ON provisioner_daemons USING btree (name, lower(COALESCE((tags ->> 'owner'::text), ''::text))); - UniqueIndexUserLinkLinkedID UniqueConstraint = "idx_user_link_linked_id" // CREATE UNIQUE INDEX idx_user_link_linked_id ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); UniqueIndexUsersEmail UniqueConstraint = "idx_users_email" // CREATE UNIQUE INDEX idx_users_email ON users USING btree (email) WHERE (deleted = false); UniqueIndexUsersUsername UniqueConstraint = "idx_users_username" // CREATE UNIQUE INDEX idx_users_username ON users USING btree (username) WHERE (deleted = false); UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true); UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id); UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false); + UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::text); UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE (deleted = false); UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false); UniqueWorkspaceProxiesLowerNameIndex UniqueConstraint = "workspace_proxies_lower_name_idx" // CREATE UNIQUE INDEX workspace_proxies_lower_name_idx ON workspace_proxies USING btree (lower(name)) WHERE (deleted = false);