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 | 99 | "when": 1778179800000, |
| 100 | 100 | "tag": "0013_cleanup_orphan_oauth_rows", |
| 101 | 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 | 64 | "when": 1778128200000, |
| 65 | 65 | "tag": "0008_scoped_credentials_cutover", |
| 66 | 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 | 6 | import { Schema } from "effect"; |
| 7 | 7 | |
| 8 | 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 | 14 | let workDir: string; |
| 11 | 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 | +}); | |