Unified · Split
| @@ -0,0 +1,241 @@ | |||
| 1 | -- Repair OpenAPI OAuth residue from the scoped-credential cutover. | ||
| 2 | -- | ||
| 3 | -- 0009 only normalized provider='openapi:oauth2' rows whose provider_state | ||
| 4 | -- still had the old `flow` shape. Some live rows already had canonical | ||
| 5 | -- `kind` provider_state, so the provider key was skipped. 0012 also collapsed | ||
| 6 | -- user-scoped OpenAPI OAuth client credential bindings when the backing Secret | ||
| 7 | -- row lived at the source/org scope. Runtime refresh reads provider_state | ||
| 8 | -- directly, but edit/refresh UI reads credential_binding rows, so recreate | ||
| 9 | -- those explicit bindings from the already-canonical Connection rows. | ||
| 10 | |||
| 11 | UPDATE "connection" | ||
| 12 | SET "provider" = 'oauth2', | ||
| 13 | "updated_at" = now() | ||
| 14 | WHERE "provider" = 'openapi:oauth2' | ||
| 15 | AND "provider_state" ? 'kind';--> statement-breakpoint | ||
| 16 | |||
| 17 | WITH oauth_connections AS ( | ||
| 18 | SELECT | ||
| 19 | b."scope_id", | ||
| 20 | b."id" AS "binding_id", | ||
| 21 | b."plugin_id", | ||
| 22 | b."source_id", | ||
| 23 | b."source_scope_id", | ||
| 24 | s."oauth2"->>'clientIdSlot' AS "slot_key", | ||
| 25 | c."provider_state"->>'clientIdSecretId' AS "secret_id", | ||
| 26 | b."created_at" | ||
| 27 | FROM "credential_binding" b | ||
| 28 | JOIN "openapi_source" s | ||
| 29 | ON s."id" = b."source_id" | ||
| 30 | AND s."scope_id" = b."source_scope_id" | ||
| 31 | JOIN "connection" c | ||
| 32 | ON c."id" = b."connection_id" | ||
| 33 | AND c."scope_id" = b."scope_id" | ||
| 34 | WHERE b."plugin_id" = 'openapi' | ||
| 35 | AND b."kind" = 'connection' | ||
| 36 | AND s."oauth2" IS NOT NULL | ||
| 37 | AND c."provider" = 'oauth2' | ||
| 38 | AND c."provider_state" ? 'clientIdSecretId' | ||
| 39 | AND coalesce(c."provider_state"->>'clientIdSecretId', '') <> '' | ||
| 40 | ) | ||
| 41 | UPDATE "credential_binding" b | ||
| 42 | SET "secret_id" = r."secret_id", | ||
| 43 | "text_value" = NULL, | ||
| 44 | "connection_id" = NULL, | ||
| 45 | "updated_at" = now() | ||
| 46 | FROM oauth_connections r | ||
| 47 | WHERE b."scope_id" = r."scope_id" | ||
| 48 | AND b."plugin_id" = r."plugin_id" | ||
| 49 | AND b."source_id" = r."source_id" | ||
| 50 | AND b."source_scope_id" = r."source_scope_id" | ||
| 51 | AND b."slot_key" = r."slot_key" | ||
| 52 | AND b."kind" = 'secret';--> statement-breakpoint | ||
| 53 | |||
| 54 | WITH oauth_connections AS ( | ||
| 55 | SELECT | ||
| 56 | b."scope_id", | ||
| 57 | b."id" AS "binding_id", | ||
| 58 | b."plugin_id", | ||
| 59 | b."source_id", | ||
| 60 | b."source_scope_id", | ||
| 61 | s."oauth2"->>'clientIdSlot' AS "slot_key", | ||
| 62 | c."provider_state"->>'clientIdSecretId' AS "secret_id", | ||
| 63 | b."created_at" | ||
| 64 | FROM "credential_binding" b | ||
| 65 | JOIN "openapi_source" s | ||
| 66 | ON s."id" = b."source_id" | ||
| 67 | AND s."scope_id" = b."source_scope_id" | ||
| 68 | JOIN "connection" c | ||
| 69 | ON c."id" = b."connection_id" | ||
| 70 | AND c."scope_id" = b."scope_id" | ||
| 71 | WHERE b."plugin_id" = 'openapi' | ||
| 72 | AND b."kind" = 'connection' | ||
| 73 | AND s."oauth2" IS NOT NULL | ||
| 74 | AND c."provider" = 'oauth2' | ||
| 75 | AND c."provider_state" ? 'clientIdSecretId' | ||
| 76 | AND coalesce(c."provider_state"->>'clientIdSecretId', '') <> '' | ||
| 77 | ) | ||
| 78 | INSERT INTO "credential_binding" ( | ||
| 79 | "id", | ||
| 80 | "scope_id", | ||
| 81 | "plugin_id", | ||
| 82 | "source_id", | ||
| 83 | "source_scope_id", | ||
| 84 | "slot_key", | ||
| 85 | "kind", | ||
| 86 | "text_value", | ||
| 87 | "secret_id", | ||
| 88 | "connection_id", | ||
| 89 | "created_at", | ||
| 90 | "updated_at" | ||
| 91 | ) | ||
| 92 | SELECT | ||
| 93 | '[' || to_jsonb('oconn-openapi-oauth-client'::text)::text || ',' || to_jsonb(r."binding_id")::text || ',' || to_jsonb(r."scope_id")::text || ',' || to_jsonb(r."slot_key")::text || ']', | ||
| 94 | r."scope_id", | ||
| 95 | r."plugin_id", | ||
| 96 | r."source_id", | ||
| 97 | r."source_scope_id", | ||
| 98 | r."slot_key", | ||
| 99 | 'secret', | ||
| 100 | NULL, | ||
| 101 | r."secret_id", | ||
| 102 | NULL, | ||
| 103 | r."created_at", | ||
| 104 | now() | ||
| 105 | FROM oauth_connections r | ||
| 106 | WHERE r."slot_key" IS NOT NULL | ||
| 107 | AND NOT EXISTS ( | ||
| 108 | SELECT 1 | ||
| 109 | FROM "credential_binding" existing | ||
| 110 | WHERE existing."scope_id" = r."scope_id" | ||
| 111 | AND existing."plugin_id" = r."plugin_id" | ||
| 112 | AND existing."source_id" = r."source_id" | ||
| 113 | AND existing."source_scope_id" = r."source_scope_id" | ||
| 114 | AND existing."slot_key" = r."slot_key" | ||
| 115 | ) | ||
| 116 | ON CONFLICT ("scope_id", "id") DO UPDATE SET | ||
| 117 | "plugin_id" = excluded."plugin_id", | ||
| 118 | "source_id" = excluded."source_id", | ||
| 119 | "source_scope_id" = excluded."source_scope_id", | ||
| 120 | "slot_key" = excluded."slot_key", | ||
| 121 | "kind" = excluded."kind", | ||
| 122 | "text_value" = excluded."text_value", | ||
| 123 | "secret_id" = excluded."secret_id", | ||
| 124 | "connection_id" = excluded."connection_id", | ||
| 125 | "updated_at" = excluded."updated_at";--> statement-breakpoint | ||
| 126 | |||
| 127 | WITH oauth_connections AS ( | ||
| 128 | SELECT | ||
| 129 | b."scope_id", | ||
| 130 | b."id" AS "binding_id", | ||
| 131 | b."plugin_id", | ||
| 132 | b."source_id", | ||
| 133 | b."source_scope_id", | ||
| 134 | coalesce( | ||
| 135 | s."oauth2"->>'clientSecretSlot', | ||
| 136 | 'oauth2:' || COALESCE(NULLIF(trim(both '-' from lower(regexp_replace(trim(s."oauth2"->>'securitySchemeName'), '[^a-zA-Z0-9]+', '-', 'g'))), ''), 'default') || ':client-secret' | ||
| 137 | ) AS "slot_key", | ||
| 138 | c."provider_state"->>'clientSecretSecretId' AS "secret_id", | ||
| 139 | b."created_at" | ||
| 140 | FROM "credential_binding" b | ||
| 141 | JOIN "openapi_source" s | ||
| 142 | ON s."id" = b."source_id" | ||
| 143 | AND s."scope_id" = b."source_scope_id" | ||
| 144 | JOIN "connection" c | ||
| 145 | ON c."id" = b."connection_id" | ||
| 146 | AND c."scope_id" = b."scope_id" | ||
| 147 | WHERE b."plugin_id" = 'openapi' | ||
| 148 | AND b."kind" = 'connection' | ||
| 149 | AND s."oauth2" IS NOT NULL | ||
| 150 | AND c."provider" = 'oauth2' | ||
| 151 | AND c."provider_state" ? 'clientSecretSecretId' | ||
| 152 | AND coalesce(c."provider_state"->>'clientSecretSecretId', '') <> '' | ||
| 153 | ) | ||
| 154 | UPDATE "credential_binding" b | ||
| 155 | SET "secret_id" = r."secret_id", | ||
| 156 | "text_value" = NULL, | ||
| 157 | "connection_id" = NULL, | ||
| 158 | "updated_at" = now() | ||
| 159 | FROM oauth_connections r | ||
| 160 | WHERE b."scope_id" = r."scope_id" | ||
| 161 | AND b."plugin_id" = r."plugin_id" | ||
| 162 | AND b."source_id" = r."source_id" | ||
| 163 | AND b."source_scope_id" = r."source_scope_id" | ||
| 164 | AND b."slot_key" = r."slot_key" | ||
| 165 | AND b."kind" = 'secret';--> statement-breakpoint | ||
| 166 | |||
| 167 | WITH oauth_connections AS ( | ||
| 168 | SELECT | ||
| 169 | b."scope_id", | ||
| 170 | b."id" AS "binding_id", | ||
| 171 | b."plugin_id", | ||
| 172 | b."source_id", | ||
| 173 | b."source_scope_id", | ||
| 174 | coalesce( | ||
| 175 | s."oauth2"->>'clientSecretSlot', | ||
| 176 | 'oauth2:' || COALESCE(NULLIF(trim(both '-' from lower(regexp_replace(trim(s."oauth2"->>'securitySchemeName'), '[^a-zA-Z0-9]+', '-', 'g'))), ''), 'default') || ':client-secret' | ||
| 177 | ) AS "slot_key", | ||
| 178 | c."provider_state"->>'clientSecretSecretId' AS "secret_id", | ||
| 179 | b."created_at" | ||
| 180 | FROM "credential_binding" b | ||
| 181 | JOIN "openapi_source" s | ||
| 182 | ON s."id" = b."source_id" | ||
| 183 | AND s."scope_id" = b."source_scope_id" | ||
| 184 | JOIN "connection" c | ||
| 185 | ON c."id" = b."connection_id" | ||
| 186 | AND c."scope_id" = b."scope_id" | ||
| 187 | WHERE b."plugin_id" = 'openapi' | ||
| 188 | AND b."kind" = 'connection' | ||
| 189 | AND s."oauth2" IS NOT NULL | ||
| 190 | AND c."provider" = 'oauth2' | ||
| 191 | AND c."provider_state" ? 'clientSecretSecretId' | ||
| 192 | AND coalesce(c."provider_state"->>'clientSecretSecretId', '') <> '' | ||
| 193 | ) | ||
| 194 | INSERT INTO "credential_binding" ( | ||
| 195 | "id", | ||
| 196 | "scope_id", | ||
| 197 | "plugin_id", | ||
| 198 | "source_id", | ||
| 199 | "source_scope_id", | ||
| 200 | "slot_key", | ||
| 201 | "kind", | ||
| 202 | "text_value", | ||
| 203 | "secret_id", | ||
| 204 | "connection_id", | ||
| 205 | "created_at", | ||
| 206 | "updated_at" | ||
| 207 | ) | ||
| 208 | SELECT | ||
| 209 | '[' || to_jsonb('oconn-openapi-oauth-secret'::text)::text || ',' || to_jsonb(r."binding_id")::text || ',' || to_jsonb(r."scope_id")::text || ',' || to_jsonb(r."slot_key")::text || ']', | ||
| 210 | r."scope_id", | ||
| 211 | r."plugin_id", | ||
| 212 | r."source_id", | ||
| 213 | r."source_scope_id", | ||
| 214 | r."slot_key", | ||
| 215 | 'secret', | ||
| 216 | NULL, | ||
| 217 | r."secret_id", | ||
| 218 | NULL, | ||
| 219 | r."created_at", | ||
| 220 | now() | ||
| 221 | FROM oauth_connections r | ||
| 222 | WHERE r."slot_key" IS NOT NULL | ||
| 223 | AND NOT EXISTS ( | ||
| 224 | SELECT 1 | ||
| 225 | FROM "credential_binding" existing | ||
| 226 | WHERE existing."scope_id" = r."scope_id" | ||
| 227 | AND existing."plugin_id" = r."plugin_id" | ||
| 228 | AND existing."source_id" = r."source_id" | ||
| 229 | AND existing."source_scope_id" = r."source_scope_id" | ||
| 230 | AND existing."slot_key" = r."slot_key" | ||
| 231 | ) | ||
| 232 | ON CONFLICT ("scope_id", "id") DO UPDATE SET | ||
| 233 | "plugin_id" = excluded."plugin_id", | ||
| 234 | "source_id" = excluded."source_id", | ||
| 235 | "source_scope_id" = excluded."source_scope_id", | ||
| 236 | "slot_key" = excluded."slot_key", | ||
| 237 | "kind" = excluded."kind", | ||
| 238 | "text_value" = excluded."text_value", | ||
| 239 | "secret_id" = excluded."secret_id", | ||
| 240 | "connection_id" = excluded."connection_id", | ||
| 241 | "updated_at" = excluded."updated_at"; | ||
| @@ -99,6 +99,13 @@ · 98 unmodified lines | |||
| 99 | "when": 1778179800000, | 99 | "when": 1778179800000, |
| 100 | "tag": "0013_cleanup_orphan_oauth_rows", | 100 | "tag": "0013_cleanup_orphan_oauth_rows", |
| 101 | "breakpoints": true | 101 | "breakpoints": true |
| 102 | }, | ||
| 103 | { | ||
| 104 | "idx": 14, | ||
| 105 | "version": "7", | ||
| 106 | "when": 1778192434062, | ||
| 107 | "tag": "0014_repair_openapi_oauth_cutover_residue", | ||
| 108 | "breakpoints": true | ||
| 102 | } | 109 | } |
| 103 | ] | 110 | ] |
| 104 | } | 111 | } |
| @@ -0,0 +1,253 @@ | |||
| 1 | -- Repair OpenAPI OAuth residue from the scoped-credential cutover. | ||
| 2 | |||
| 3 | UPDATE `connection` | ||
| 4 | SET `provider` = 'oauth2', | ||
| 5 | `updated_at` = CAST(strftime('%s', 'now') AS INTEGER) * 1000 | ||
| 6 | WHERE `provider` = 'openapi:oauth2' | ||
| 7 | AND json_extract(`provider_state`, '$.kind') IS NOT NULL;--> statement-breakpoint | ||
| 8 | |||
| 9 | WITH oauth_connections AS ( | ||
| 10 | SELECT | ||
| 11 | b.`scope_id`, | ||
| 12 | b.`id` AS `binding_id`, | ||
| 13 | b.`plugin_id`, | ||
| 14 | b.`source_id`, | ||
| 15 | b.`source_scope_id`, | ||
| 16 | json_extract(s.`oauth2`, '$.clientIdSlot') AS `slot_key`, | ||
| 17 | json_extract(c.`provider_state`, '$.clientIdSecretId') AS `secret_id`, | ||
| 18 | b.`created_at` | ||
| 19 | FROM `credential_binding` b | ||
| 20 | JOIN `openapi_source` s | ||
| 21 | ON s.`id` = b.`source_id` | ||
| 22 | AND s.`scope_id` = b.`source_scope_id` | ||
| 23 | JOIN `connection` c | ||
| 24 | ON c.`id` = b.`connection_id` | ||
| 25 | AND c.`scope_id` = b.`scope_id` | ||
| 26 | WHERE b.`plugin_id` = 'openapi' | ||
| 27 | AND b.`kind` = 'connection' | ||
| 28 | AND s.`oauth2` IS NOT NULL | ||
| 29 | AND c.`provider` = 'oauth2' | ||
| 30 | AND json_extract(c.`provider_state`, '$.clientIdSecretId') IS NOT NULL | ||
| 31 | AND coalesce(json_extract(c.`provider_state`, '$.clientIdSecretId'), '') <> '' | ||
| 32 | ) | ||
| 33 | UPDATE `credential_binding` | ||
| 34 | SET | ||
| 35 | `secret_id` = ( | ||
| 36 | SELECT r.`secret_id` | ||
| 37 | FROM oauth_connections r | ||
| 38 | WHERE r.`scope_id` = `credential_binding`.`scope_id` | ||
| 39 | AND r.`plugin_id` = `credential_binding`.`plugin_id` | ||
| 40 | AND r.`source_id` = `credential_binding`.`source_id` | ||
| 41 | AND r.`source_scope_id` = `credential_binding`.`source_scope_id` | ||
| 42 | AND r.`slot_key` = `credential_binding`.`slot_key` | ||
| 43 | LIMIT 1 | ||
| 44 | ), | ||
| 45 | `text_value` = NULL, | ||
| 46 | `connection_id` = NULL, | ||
| 47 | `updated_at` = CAST(strftime('%s', 'now') AS INTEGER) * 1000 | ||
| 48 | WHERE `kind` = 'secret' | ||
| 49 | AND EXISTS ( | ||
| 50 | SELECT 1 | ||
| 51 | FROM oauth_connections r | ||
| 52 | WHERE r.`scope_id` = `credential_binding`.`scope_id` | ||
| 53 | AND r.`plugin_id` = `credential_binding`.`plugin_id` | ||
| 54 | AND r.`source_id` = `credential_binding`.`source_id` | ||
| 55 | AND r.`source_scope_id` = `credential_binding`.`source_scope_id` | ||
| 56 | AND r.`slot_key` = `credential_binding`.`slot_key` | ||
| 57 | );--> statement-breakpoint | ||
| 58 | |||
| 59 | WITH oauth_connections AS ( | ||
| 60 | SELECT | ||
| 61 | b.`scope_id`, | ||
| 62 | b.`id` AS `binding_id`, | ||
| 63 | b.`plugin_id`, | ||
| 64 | b.`source_id`, | ||
| 65 | b.`source_scope_id`, | ||
| 66 | json_extract(s.`oauth2`, '$.clientIdSlot') AS `slot_key`, | ||
| 67 | json_extract(c.`provider_state`, '$.clientIdSecretId') AS `secret_id`, | ||
| 68 | b.`created_at` | ||
| 69 | FROM `credential_binding` b | ||
| 70 | JOIN `openapi_source` s | ||
| 71 | ON s.`id` = b.`source_id` | ||
| 72 | AND s.`scope_id` = b.`source_scope_id` | ||
| 73 | JOIN `connection` c | ||
| 74 | ON c.`id` = b.`connection_id` | ||
| 75 | AND c.`scope_id` = b.`scope_id` | ||
| 76 | WHERE b.`plugin_id` = 'openapi' | ||
| 77 | AND b.`kind` = 'connection' | ||
| 78 | AND s.`oauth2` IS NOT NULL | ||
| 79 | AND c.`provider` = 'oauth2' | ||
| 80 | AND json_extract(c.`provider_state`, '$.clientIdSecretId') IS NOT NULL | ||
| 81 | AND coalesce(json_extract(c.`provider_state`, '$.clientIdSecretId'), '') <> '' | ||
| 82 | ) | ||
| 83 | INSERT INTO `credential_binding` ( | ||
| 84 | `id`, | ||
| 85 | `scope_id`, | ||
| 86 | `plugin_id`, | ||
| 87 | `source_id`, | ||
| 88 | `source_scope_id`, | ||
| 89 | `slot_key`, | ||
| 90 | `kind`, | ||
| 91 | `text_value`, | ||
| 92 | `secret_id`, | ||
| 93 | `connection_id`, | ||
| 94 | `created_at`, | ||
| 95 | `updated_at` | ||
| 96 | ) | ||
| 97 | SELECT | ||
| 98 | json_array('oconn-openapi-oauth-client', r.`binding_id`, r.`scope_id`, r.`slot_key`), | ||
| 99 | r.`scope_id`, | ||
| 100 | r.`plugin_id`, | ||
| 101 | r.`source_id`, | ||
| 102 | r.`source_scope_id`, | ||
| 103 | r.`slot_key`, | ||
| 104 | 'secret', | ||
| 105 | NULL, | ||
| 106 | r.`secret_id`, | ||
| 107 | NULL, | ||
| 108 | r.`created_at`, | ||
| 109 | CAST(strftime('%s', 'now') AS INTEGER) * 1000 | ||
| 110 | FROM oauth_connections r | ||
| 111 | WHERE r.`slot_key` IS NOT NULL | ||
| 112 | AND NOT EXISTS ( | ||
| 113 | SELECT 1 | ||
| 114 | FROM `credential_binding` existing | ||
| 115 | WHERE existing.`scope_id` = r.`scope_id` | ||
| 116 | AND existing.`plugin_id` = r.`plugin_id` | ||
| 117 | AND existing.`source_id` = r.`source_id` | ||
| 118 | AND existing.`source_scope_id` = r.`source_scope_id` | ||
| 119 | AND existing.`slot_key` = r.`slot_key` | ||
| 120 | ) | ||
| 121 | ON CONFLICT(`scope_id`, `id`) DO UPDATE SET | ||
| 122 | `plugin_id` = excluded.`plugin_id`, | ||
| 123 | `source_id` = excluded.`source_id`, | ||
| 124 | `source_scope_id` = excluded.`source_scope_id`, | ||
| 125 | `slot_key` = excluded.`slot_key`, | ||
| 126 | `kind` = excluded.`kind`, | ||
| 127 | `text_value` = excluded.`text_value`, | ||
| 128 | `secret_id` = excluded.`secret_id`, | ||
| 129 | `connection_id` = excluded.`connection_id`, | ||
| 130 | `updated_at` = excluded.`updated_at`;--> statement-breakpoint | ||
| 131 | |||
| 132 | WITH oauth_connections AS ( | ||
| 133 | SELECT | ||
| 134 | b.`scope_id`, | ||
| 135 | b.`id` AS `binding_id`, | ||
| 136 | b.`plugin_id`, | ||
| 137 | b.`source_id`, | ||
| 138 | b.`source_scope_id`, | ||
| 139 | json_extract(s.`oauth2`, '$.clientSecretSlot') AS `slot_key`, | ||
| 140 | json_extract(c.`provider_state`, '$.clientSecretSecretId') AS `secret_id`, | ||
| 141 | b.`created_at` | ||
| 142 | FROM `credential_binding` b | ||
| 143 | JOIN `openapi_source` s | ||
| 144 | ON s.`id` = b.`source_id` | ||
| 145 | AND s.`scope_id` = b.`source_scope_id` | ||
| 146 | JOIN `connection` c | ||
| 147 | ON c.`id` = b.`connection_id` | ||
| 148 | AND c.`scope_id` = b.`scope_id` | ||
| 149 | WHERE b.`plugin_id` = 'openapi' | ||
| 150 | AND b.`kind` = 'connection' | ||
| 151 | AND s.`oauth2` IS NOT NULL | ||
| 152 | AND c.`provider` = 'oauth2' | ||
| 153 | AND json_extract(c.`provider_state`, '$.clientSecretSecretId') IS NOT NULL | ||
| 154 | AND coalesce(json_extract(c.`provider_state`, '$.clientSecretSecretId'), '') <> '' | ||
| 155 | ) | ||
| 156 | UPDATE `credential_binding` | ||
| 157 | SET | ||
| 158 | `secret_id` = ( | ||
| 159 | SELECT r.`secret_id` | ||
| 160 | FROM oauth_connections r | ||
| 161 | WHERE r.`scope_id` = `credential_binding`.`scope_id` | ||
| 162 | AND r.`plugin_id` = `credential_binding`.`plugin_id` | ||
| 163 | AND r.`source_id` = `credential_binding`.`source_id` | ||
| 164 | AND r.`source_scope_id` = `credential_binding`.`source_scope_id` | ||
| 165 | AND r.`slot_key` = `credential_binding`.`slot_key` | ||
| 166 | LIMIT 1 | ||
| 167 | ), | ||
| 168 | `text_value` = NULL, | ||
| 169 | `connection_id` = NULL, | ||
| 170 | `updated_at` = CAST(strftime('%s', 'now') AS INTEGER) * 1000 | ||
| 171 | WHERE `kind` = 'secret' | ||
| 172 | AND EXISTS ( | ||
| 173 | SELECT 1 | ||
| 174 | FROM oauth_connections r | ||
| 175 | WHERE r.`scope_id` = `credential_binding`.`scope_id` | ||
| 176 | AND r.`plugin_id` = `credential_binding`.`plugin_id` | ||
| 177 | AND r.`source_id` = `credential_binding`.`source_id` | ||
| 178 | AND r.`source_scope_id` = `credential_binding`.`source_scope_id` | ||
| 179 | AND r.`slot_key` = `credential_binding`.`slot_key` | ||
| 180 | );--> statement-breakpoint | ||
| 181 | |||
| 182 | WITH oauth_connections AS ( | ||
| 183 | SELECT | ||
| 184 | b.`scope_id`, | ||
| 185 | b.`id` AS `binding_id`, | ||
| 186 | b.`plugin_id`, | ||
| 187 | b.`source_id`, | ||
| 188 | b.`source_scope_id`, | ||
| 189 | json_extract(s.`oauth2`, '$.clientSecretSlot') AS `slot_key`, | ||
| 190 | json_extract(c.`provider_state`, '$.clientSecretSecretId') AS `secret_id`, | ||
| 191 | b.`created_at` | ||
| 192 | FROM `credential_binding` b | ||
| 193 | JOIN `openapi_source` s | ||
| 194 | ON s.`id` = b.`source_id` | ||
| 195 | AND s.`scope_id` = b.`source_scope_id` | ||
| 196 | JOIN `connection` c | ||
| 197 | ON c.`id` = b.`connection_id` | ||
| 198 | AND c.`scope_id` = b.`scope_id` | ||
| 199 | WHERE b.`plugin_id` = 'openapi' | ||
| 200 | AND b.`kind` = 'connection' | ||
| 201 | AND s.`oauth2` IS NOT NULL | ||
| 202 | AND c.`provider` = 'oauth2' | ||
| 203 | AND json_extract(c.`provider_state`, '$.clientSecretSecretId') IS NOT NULL | ||
| 204 | AND coalesce(json_extract(c.`provider_state`, '$.clientSecretSecretId'), '') <> '' | ||
| 205 | ) | ||
| 206 | INSERT INTO `credential_binding` ( | ||
| 207 | `id`, | ||
| 208 | `scope_id`, | ||
| 209 | `plugin_id`, | ||
| 210 | `source_id`, | ||
| 211 | `source_scope_id`, | ||
| 212 | `slot_key`, | ||
| 213 | `kind`, | ||
| 214 | `text_value`, | ||
| 215 | `secret_id`, | ||
| 216 | `connection_id`, | ||
| 217 | `created_at`, | ||
| 218 | `updated_at` | ||
| 219 | ) | ||
| 220 | SELECT | ||
| 221 | json_array('oconn-openapi-oauth-secret', r.`binding_id`, r.`scope_id`, r.`slot_key`), | ||
| 222 | r.`scope_id`, | ||
| 223 | r.`plugin_id`, | ||
| 224 | r.`source_id`, | ||
| 225 | r.`source_scope_id`, | ||
| 226 | r.`slot_key`, | ||
| 227 | 'secret', | ||
| 228 | NULL, | ||
| 229 | r.`secret_id`, | ||
| 230 | NULL, | ||
| 231 | r.`created_at`, | ||
| 232 | CAST(strftime('%s', 'now') AS INTEGER) * 1000 | ||
| 233 | FROM oauth_connections r | ||
| 234 | WHERE r.`slot_key` IS NOT NULL | ||
| 235 | AND NOT EXISTS ( | ||
| 236 | SELECT 1 | ||
| 237 | FROM `credential_binding` existing | ||
| 238 | WHERE existing.`scope_id` = r.`scope_id` | ||
| 239 | AND existing.`plugin_id` = r.`plugin_id` | ||
| 240 | AND existing.`source_id` = r.`source_id` | ||
| 241 | AND existing.`source_scope_id` = r.`source_scope_id` | ||
| 242 | AND existing.`slot_key` = r.`slot_key` | ||
| 243 | ) | ||
| 244 | ON CONFLICT(`scope_id`, `id`) DO UPDATE SET | ||
| 245 | `plugin_id` = excluded.`plugin_id`, | ||
| 246 | `source_id` = excluded.`source_id`, | ||
| 247 | `source_scope_id` = excluded.`source_scope_id`, | ||
| 248 | `slot_key` = excluded.`slot_key`, | ||
| 249 | `kind` = excluded.`kind`, | ||
| 250 | `text_value` = excluded.`text_value`, | ||
| 251 | `secret_id` = excluded.`secret_id`, | ||
| 252 | `connection_id` = excluded.`connection_id`, | ||
| 253 | `updated_at` = excluded.`updated_at`; | ||
| @@ -64,6 +64,13 @@ · 63 unmodified lines | |||
| 64 | "when": 1778128200000, | 64 | "when": 1778128200000, |
| 65 | "tag": "0008_scoped_credentials_cutover", | 65 | "tag": "0008_scoped_credentials_cutover", |
| 66 | "breakpoints": true | 66 | "breakpoints": true |
| 67 | }, | ||
| 68 | { | ||
| 69 | "idx": 9, | ||
| 70 | "version": "6", | ||
| 71 | "when": 1778192434062, | ||
| 72 | "tag": "0009_repair_openapi_oauth_cutover_residue", | ||
| 73 | "breakpoints": true | ||
| 67 | } | 74 | } |
| 68 | ] | 75 | ] |
| 69 | } | 76 | } |
| @@ -6,6 +6,10 @@ import { join } from "node:path"; · 5 unmodified lines | |||
| 6 | import { Schema } from "effect"; | 6 | import { Schema } from "effect"; |
| 7 | 7 | ||
| 8 | const MIGRATION = join(import.meta.dirname, "../../drizzle/0008_scoped_credentials_cutover.sql"); | 8 | const MIGRATION = join(import.meta.dirname, "../../drizzle/0008_scoped_credentials_cutover.sql"); |
| 9 | const REPAIR_MIGRATION = join( | ||
| 10 | import.meta.dirname, | ||
| 11 | "../../drizzle/0009_repair_openapi_oauth_cutover_residue.sql", | ||
| 12 | ); | ||
| 9 | 13 | ||
| 10 | let workDir: string; | 14 | let workDir: string; |
| 11 | let db: Database; | 15 | let db: Database; |
| @@ -126,3 +130,193 @@ describe("0008_scoped_credentials_cutover OAuth connection section", () => { · 114 unmodified lines | |||
| 126 | }); | 130 | }); |
| 127 | }); | 131 | }); |
| 128 | }); | 132 | }); |
| 133 | |||
| 134 | describe("0009_repair_openapi_oauth_cutover_residue", () => { | ||
| 135 | it("repairs already-canonical OpenAPI rows and restores user-scoped OAuth secret bindings", () => { | ||
| 136 | const now = Date.now(); | ||
| 137 | db.exec(` | ||
| 138 | CREATE TABLE \`openapi_source\` ( | ||
| 139 | \`id\` text NOT NULL, | ||
| 140 | \`scope_id\` text NOT NULL, | ||
| 141 | \`oauth2\` text, | ||
| 142 | PRIMARY KEY(\`scope_id\`, \`id\`) | ||
| 143 | ); | ||
| 144 | CREATE TABLE \`credential_binding\` ( | ||
| 145 | \`id\` text NOT NULL, | ||
| 146 | \`scope_id\` text NOT NULL, | ||
| 147 | \`plugin_id\` text NOT NULL, | ||
| 148 | \`source_id\` text NOT NULL, | ||
| 149 | \`source_scope_id\` text NOT NULL, | ||
| 150 | \`slot_key\` text NOT NULL, | ||
| 151 | \`kind\` text NOT NULL, | ||
| 152 | \`text_value\` text, | ||
| 153 | \`secret_id\` text, | ||
| 154 | \`connection_id\` text, | ||
| 155 | \`created_at\` integer NOT NULL, | ||
| 156 | \`updated_at\` integer NOT NULL, | ||
| 157 | PRIMARY KEY(\`scope_id\`, \`id\`) | ||
| 158 | ); | ||
| 159 | `); | ||
| 160 | |||
| 161 | db.prepare( | ||
| 162 | "INSERT INTO `openapi_source` (id, scope_id, oauth2) VALUES (?, ?, ?)", | ||
| 163 | ).run( | ||
| 164 | "dealcloud_api", | ||
| 165 | "org-1", | ||
| 166 | JSON.stringify({ | ||
| 167 | kind: "oauth2", | ||
| 168 | securitySchemeName: "oauth2", | ||
| 169 | clientIdSlot: "oauth2:oauth2:client-id", | ||
| 170 | clientSecretSlot: "oauth2:oauth2:client-secret", | ||
| 171 | connectionSlot: "oauth2:oauth2:connection", | ||
| 172 | }), | ||
| 173 | ); | ||
| 174 | |||
| 175 | const insertConnection = db.prepare( | ||
| 176 | "INSERT INTO `connection` (id, scope_id, provider, provider_state, scope, updated_at) VALUES (?, ?, ?, ?, ?, ?)", | ||
| 177 | ); | ||
| 178 | insertConnection.run( | ||
| 179 | "openapi-oauth2-app-dealcloud_api", | ||
| 180 | "org-1", | ||
| 181 | "oauth2", | ||
| 182 | JSON.stringify({ | ||
| 183 | kind: "client-credentials", | ||
| 184 | tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | ||
| 185 | clientIdSecretId: "dealcloud-client-id", | ||
| 186 | clientSecretSecretId: "dealcloud-client-secret", | ||
| 187 | }), | ||
| 188 | null, | ||
| 189 | now, | ||
| 190 | ); | ||
| 191 | insertConnection.run( | ||
| 192 | "openapi-oauth2-app-dealcloud_api", | ||
| 193 | "user-org:user-jd:org-1", | ||
| 194 | "openapi:oauth2", | ||
| 195 | JSON.stringify({ | ||
| 196 | kind: "client-credentials", | ||
| 197 | tokenEndpoint: "https://resolve.dealcloud.com/oauth/token", | ||
| 198 | clientIdSecretId: "dealcloud-client-id-jd", | ||
| 199 | clientSecretSecretId: "dealcloud-client-secret-jd", | ||
| 200 | }), | ||
| 201 | null, | ||
| 202 | now, | ||
| 203 | ); | ||
| 204 | |||
| 205 | const insertBinding = db.prepare( | ||
| 206 | "INSERT INTO `credential_binding` (id, scope_id, plugin_id, source_id, source_scope_id, slot_key, kind, text_value, secret_id, connection_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||
| 207 | ); | ||
| 208 | insertBinding.run( | ||
| 209 | "org-client-id", | ||
| 210 | "org-1", | ||
| 211 | "openapi", | ||
| 212 | "dealcloud_api", | ||
| 213 | "org-1", | ||
| 214 | "oauth2:oauth2:client-id", | ||
| 215 | "secret", | ||
| 216 | null, | ||
| 217 | "dealcloud-client-id-jd", | ||
| 218 | null, | ||
| 219 | now, | ||
| 220 | now, | ||
| 221 | ); | ||
| 222 | insertBinding.run( | ||
| 223 | "org-client-secret", | ||
| 224 | "org-1", | ||
| 225 | "openapi", | ||
| 226 | "dealcloud_api", | ||
| 227 | "org-1", | ||
| 228 | "oauth2:oauth2:client-secret", | ||
| 229 | "secret", | ||
| 230 | null, | ||
| 231 | "dealcloud-client-secret-jd", | ||
| 232 | null, | ||
| 233 | now, | ||
| 234 | now, | ||
| 235 | ); | ||
| 236 | insertBinding.run( | ||
| 237 | "org-connection", | ||
| 238 | "org-1", | ||
| 239 | "openapi", | ||
| 240 | "dealcloud_api", | ||
| 241 | "org-1", | ||
| 242 | "oauth2:oauth2:connection", | ||
| 243 | "connection", | ||
| 244 | null, | ||
| 245 | null, | ||
| 246 | "openapi-oauth2-app-dealcloud_api", | ||
| 247 | now, | ||
| 248 | now, | ||
| 249 | ); | ||
| 250 | insertBinding.run( | ||
| 251 | "jd-connection", | ||
| 252 | "user-org:user-jd:org-1", | ||
| 253 | "openapi", | ||
| 254 | "dealcloud_api", | ||
| 255 | "org-1", | ||
| 256 | "oauth2:oauth2:connection", | ||
| 257 | "connection", | ||
| 258 | null, | ||
| 259 | null, | ||
| 260 | "openapi-oauth2-app-dealcloud_api", | ||
| 261 | now, | ||
| 262 | now, | ||
| 263 | ); | ||
| 264 | |||
| 265 | db.exec(readFileSync(REPAIR_MIGRATION, "utf-8")); | ||
| 266 | |||
| 267 | const providers = decodeConnectionRows( | ||
| 268 | db.prepare("SELECT provider, provider_state FROM `connection` ORDER BY scope_id").all(), | ||
| 269 | ); | ||
| 270 | expect(providers.map((row) => row.provider)).toEqual(["oauth2", "oauth2"]); | ||
| 271 | |||
| 272 | const bindings = db | ||
| 273 | .prepare( | ||
| 274 | "SELECT scope_id, slot_key, kind, secret_id, connection_id FROM `credential_binding` WHERE source_id = ? ORDER BY scope_id, slot_key", | ||
| 275 | ) | ||
| 276 | .all("dealcloud_api"); | ||
| 277 | expect(bindings).toEqual([ | ||
| 278 | { | ||
| 279 | scope_id: "org-1", | ||
| 280 | slot_key: "oauth2:oauth2:client-id", | ||
| 281 | kind: "secret", | ||
| 282 | secret_id: "dealcloud-client-id", | ||
| 283 | connection_id: null, | ||
| 284 | }, | ||
| 285 | { | ||
| 286 | scope_id: "org-1", | ||
| 287 | slot_key: "oauth2:oauth2:client-secret", | ||
| 288 | kind: "secret", | ||
| 289 | secret_id: "dealcloud-client-secret", | ||
| 290 | connection_id: null, | ||
| 291 | }, | ||
| 292 | { | ||
| 293 | scope_id: "org-1", | ||
| 294 | slot_key: "oauth2:oauth2:connection", | ||
| 295 | kind: "connection", | ||
| 296 | secret_id: null, | ||
| 297 | connection_id: "openapi-oauth2-app-dealcloud_api", | ||
| 298 | }, | ||
| 299 | { | ||
| 300 | scope_id: "user-org:user-jd:org-1", | ||
| 301 | slot_key: "oauth2:oauth2:client-id", | ||
| 302 | kind: "secret", | ||
| 303 | secret_id: "dealcloud-client-id-jd", | ||
| 304 | connection_id: null, | ||
| 305 | }, | ||
| 306 | { | ||
| 307 | scope_id: "user-org:user-jd:org-1", | ||
| 308 | slot_key: "oauth2:oauth2:client-secret", | ||
| 309 | kind: "secret", | ||
| 310 | secret_id: "dealcloud-client-secret-jd", | ||
| 311 | connection_id: null, | ||
| 312 | }, | ||
| 313 | { | ||
| 314 | scope_id: "user-org:user-jd:org-1", | ||
| 315 | slot_key: "oauth2:oauth2:connection", | ||
| 316 | kind: "connection", | ||
| 317 | secret_id: null, | ||
| 318 | connection_id: "openapi-oauth2-app-dealcloud_api", | ||
| 319 | }, | ||
| 320 | ]); | ||
| 321 | }); | ||
| 322 | }); | ||