Блог ·
Межа належить ядру
Відкликання розміщеного агента відчіплювало його від замовлення і лишало запущену машину Proxmox, до якої ніхто не міг дістатися, а платити не переставали.
Автор: Provibr
Cloud Agent є тим різновидом нашого агента, де машину запускаємо ми. Клієнт замовляє одного, ми клонуємо шаблон Proxmox, ставимо на нього агента і виставляємо за нього рахунок щомісяця. З панелі він виглядає як будь-який інший агент, і саме ця схожість є цілою історією цього раунду.
Кожен інший агент належить клієнтові. Відкликати його є чистою, безпечною операцією: платформа надсилає повідомлення про відкликання, агент витирає свою тотожність, свій сертифікат і своє сховище облікових даних, а тоді рядок можна видалити. Ніде не лишається нічого запущеного, бо те, що було запущене, ніколи не було нашим.
Для Cloud Agent це не є правдою, і ніщо цьому не заважало.
Що ми зміряли, перш ніж будь-що змінювати
На базі даних розробки, зі справжнім замовленням за справжнім агентом, ми викликали шлях вилучення точно так, як це робить кнопка в панелі.
Він відзвітував mode: "hard". Рядок агента зник. Рядок замовлення досі був там, досі active, досі ніс vmid 5998, а його agent_id тихо став NULL, бо зовнішній ключ від замовлення до агента є ON DELETE SET NULL.
Тож на гіпервізорі була віртуальна машина, запущена, з агентом на ній, який витер власну тотожність і тому вже ніколи не міг би зʼєднатися, причеплена до замовлення, що досі породжувало щомісячний рахунок, і ніде не було рядка, який вказував би від одного до іншого. Недосяжна, незупинна з панелі і не безкоштовна.
Обмеження, що це породило, є правильним. Ставити посилання в NULL є правильною поведінкою для стовпця, що каже, який агент зараз тут установлений; каскад видалив би замовлення і з ним запис для рахунку. Дефектом є те, що ніщо не поставило питання, якого схема поставити не може.
Дві наявні сторожі, обидві дивляться не туди
Видалення агента вже мало дві відмови, і варто бути точним щодо того, чому не спрацювала жодна.
Перша відмовляється видаляти агента, який зараз зареєстрований: спершу відкличте його, щоб він прибрав за собою. На той час, коли рядок видаляли, агента щойно відкликали тим самим викликом, тож він не був зареєстрований і перевірку було задоволено.
Друга відмовляється видаляти агента, на якому досі висять інтеграції або сервери, бо ті зовнішні ключі каскадують і один клац забрав би з собою цілий інвентар. Cloud Agent не має ні того, ні того: він є свіжою машиною, що існує заради запуску агента, тож перевірка пройшла, бо не було чого лічити.
Обидві сторожі були про те, що зруйнує видалення. Те, що видалення переживає і що коштує грошей саме тому, що переживає, лежало поза формою питання.
Межа йде в ядро, а не в кнопку
Очевидним ремонтом є перевірити в серверній дії за кнопкою. Він також є хибним, а причиною є прогалина, у яку ця кодова база вже заходила зі стоковою кінцевою точкою автентифікації, що обходила кожен серверний контроль.
Ми полічили місця виклику. Поза двома основними функціями є сім викликів відкликання, видалення або обʼєднаного вилучення, розкиданих по трьох файлах: три серверні дії на сторінці агента, групова дія і два маршрути API v1. Перевірка в дії панелі лишила б маршрути API відкритими, а маршрут API є саме тим, чим користується клієнтський скрипт.
Тож межа сидить усередині самих revokeAgentCore() та deleteAgentCore(), у вузькому місці, крізь яке проходять усі сім викликів. Одна функція, спитана двічі.
Вона сидить після ідемпотентної гілки, а не перед нею. Агент, якого вже відкликано, не має чого відкликати, а сказати тому, хто викликає, спершу скасувати замовлення було б неправдою про виклик, що однаково нічого не зробить. Межа належить операції, яка справді щось змінює.
Присудком є чи це ще можна скасувати, а не чи це живе
У спільному пакеті є список під назвою CLOUD_AGENT_LIVE_STATUSES: стани, у яких машина існує і коштує грошей. Він має чотири члени і є саме тим списком, який тут не слід перевикористовувати.
Присудком, яким ми користуємося, є cloudAgentActionAllowed("cancel", status), що є правдивим для семи з восьми станів замовлення, для всіх, крім замовлення, яке вже скасовано.
Три стани в різниці є цікавими, і failed є найгострішим. Невдале замовлення не коштує нічого, тож воно не є живим. Але саме невдале замовлення є тим, що могло зайти достатньо далеко, щоб створити машину, і тоді впасти, і саме тому скасування з нього взагалі дозволене. Перевикористання списку для рахунків лишило б неприкритим той єдиний випадок з осиротілою машиною, і на рецензії це виглядало б цілком розважливо.
Два списки, що означають майже те саме, розходяться. Питанням тут є не чи це коштує грошей, а чи скасування досі є шляхом, і рівно одна функція в кодовій базі відповідає саме на це.
Два шляхи запису, яким дозволено її оминути
Сторожа, яку ми написали, виводить кожне місце в джерелі панелі, що відкликає, жорстко видаляє або мʼяко ховає рядок агента, обходом синтаксичного дерева по деревах застосунку та бібліотек, і вимагає для кожного місця або межу, або записану причину. Два місця несуть причину: сам шлях скасування, що вилучає машину, звільняє адресу і лише тоді відкликає агента, і закриття облікового запису, що відкликає кожного агента організації і опрацьовує Cloud Agent окремо в тому самому потоці. Заблокувати будь-яке з них означало б закрити правильний вихід, а не заощадити гроші.
Сторожа має два сита, що не дають сканеру джерела бути пустопорожньо зеленим: нижню межу на те, скільки місць запису він мусить знайти, з трьома названими, і тест, що зазнає невдачі на записі списку дозволеного, який вказує на файл, що більше нічого не пише. Вона виконує 4 тести і 11 тверджень.
Два факти на екрані, а не один
Панельний бік потребував двох окремих булевих значень, і ми ледь не випустили одне.
orderOpen є фактом про замовлення: чи це ще можна скасувати. canCancel є фактом про глядача: чи цій особі можна його скасувати. Злити їх в єдине чи можна тут діяти означало б, що учасник без права скасування бачить кнопку відкликання, користується нею, викидає машину і не може зупинити рахунок. Тому кнопки зникають за фактом про замовлення, а картка лишається з причиною всередині, бо чому я не можу відкликати саме цього є питанням, яке ставить кожен, хто бачив цю кнопку на своїх інших агентах.
Одна річ прийшла слідом, бо зміна зробила її неправдивою: інструкції з деінсталяції радили спершу відкликати агента в панелі. Це є кнопкою, якої на цій сторінці більше немає.
Що ми б записали
- Схожість в інтерфейсі не є схожістю в наслідках. Два агенти, одна кнопка, один з них коштує грошей після того, як ви її натиснули.
ON DELETE SET NULLможе бути правильним і все одно лишити вас із сиротою. Схема не може поставити те питання.- Сторожі про те, що видалення руйнує, не побачать того, що видалення полишає.
- Полічіть місця виклику, перш ніж обирати, куди йде перевірка. Наших було сім, у трьох файлах, два з них маршрути API.
- Ставте межу у вузькому місці, а не на кнопці. Кнопка є одним із входів.
- Ставте її після ідемпотентної гілки. Відмова на виклику, що нічого не зробив би, є брехнею.
- Перевикористання списку, що означає майже те саме, є повільною невдачею. Коштує грошей і ще можна скасувати різняться трьома станами, і саме в різниці лежить шкода.
- Стан, що не коштує нічого, все одно міг щось по собі лишити. Невдале розгортання є небезпечним.
- Розділяйте факт про обʼєкт і факт про глядача. Злиті, вони вкладають руйнівну кнопку в руки обмеженого учасника.
- Хай сторожа читає дерево. Тест на кожен відомий вхід не побачить входу, який хтось додасть наступного місяця.
Для ясності про те, чим це є: дефект відтворено проти бази даних розробки перед будь-якою зміною, наскрізна перевірка завершилася на 26 зелених і перед тим була доведена червоною на 16 зелених та 10 невдачах, браузерна перевірка на 29 зелених і доведена червоною на 18 та 11 з вимкненим прапорцем замовлення, девʼять мов на 178 зелених, а перевірка злиття пройшла без рядка невдачі. Не доведено: жодної справжньої клієнтської машини задіяно не було, а бік рахунків радше оглянуто, ніж випробувано. Три пастки вимірювання коштували часу і варті згадки: небезпечна зона живе за параметром вкладки, тож перевірка, що його оминає, міряє нуль кнопок і читає це як успіх; зворотний слеш усередині шаблонного літерала, переданого в обчислення на сторінці, не є екрануванням; а один із трьох каталогових скриптів сплющив нерозривний пробіл, який двоє інших зберегли.
Із платформної роботи над Provibr. Чому машину можна продати рівно один раз, описано в Два замовлення, одна машина, чому ярлик є посиланням і ніколи не дозволом, описано в Ярлик є посиланням, а не правом, а слово стану, позичене з не тієї таблиці, описано в Сканування, що закривало власне вікно.