Модульность ваших приложений

Возможно, вы знаете о некоторых традиционных методах сокрытия секретов, таких как ключ API или токен некоторых сторонних сервисов, которые вам нужны для вашего приложения. Но в настоящее время подходы к модулированию вашего приложения с использованием SwiftPM становятся все более популярными.

Например, у Point-Free есть отличный бесплатный выпуск на эту тему, а Маджид Джабраилов недавно написал серию из 4 частей на тему Архитектура микроприложений (части 1, 2, 3, 4), в которой Я могу оба рекомендовать для начала.

Кроме того, вы можете даже захотеть скрыть секреты в общедоступных библиотеках с открытым исходным кодом, например. в модульных тестах интеграции некоторых сторонних сервисов, где пользователи библиотеки будут предоставлять свой собственный токен, но вы хотите, чтобы ваши тесты выполнялись с вашими собственными.

Что общего у этих ситуаций, так это то, что они основаны на пользовательском поддерживаемом файле Package.swift, а не на том, который Xcode поддерживает для вас, если вы просто добавляете зависимость в проект приложения. Приложение или проект разбиты на множество небольших модулей без соответствующего файла .xcodeproj, Xcode просто открывает файл Package.swift напрямую, без необходимости в проекте.

Это также означает, что для отдельных модулей нет возможности указать какие-либо параметры сборки или скрипты сборки в Xcode, все нужно сделать прямо в файле Package.swift манифеста.

Хотя в будущих версиях SwiftPM добавляется все больше и больше таких функций (например, SE-303, SE-325, SE-332), нет никаких признаков того, что они будут поддерживать какие-либо специфичные для Xcode функции, такие как файлы .xcconfig. .

Как мы можем скрыть секреты от фиксации в Git, чтобы гарантировать, что мы не передадим их нашему поставщику Git или кому-либо еще, у кого есть доступ к нашему репозиторию сегодня?

Ресурсы SwiftPM и JSONDecoder спешат на помощь

Я уверен, что нет единственного лучшего ответа на этот вопрос, и у других могут быть более умные идеи, чем у меня. Но мне нравится делать вещи простыми, и мне также нравится использовать базовые функции, потому что я хорошо их знаю и ожидаю, что другие разработчики быстро поймут их, если это необходимо. Кроме того, я могу быть уверен, что они рассчитаны на будущее.

Подход, который я хочу использовать, — это классический .env файловый подход, распространенный в веб-разработке. Но вместо файла .env пользовательского формата я хочу просто иметь файл .json с моими секретами в нем, потому что файлы JSON знакомы многим разработчикам iOS, и у нас есть встроенная поддержка их парсинга в Swift благодаря JSONDecoder . Загрузка файлов или, в более общем смысле, ресурсов также поддерживается SwiftPM, начиная с версии Swift 5.3 (SE-271).

Вот основная идея того, как я хочу скрыть секреты от Git:

  1. Зарегистрируйте файл secrets.json.sample в Git с ключами, но без значений
  2. Позвольте разработчикам продублировать его, удалить расширение .sample и добавить значения.
  3. Игнорировать файл secrets.json через .gitignore, чтобы он никогда не возвращался
  4. Предоставьте простой struct соответствующий Decodable для чтения секретов

Остальная часть этой статьи представляет собой пошаговое руководство по применению этого подхода. В качестве примера я буду использовать модульные тесты моего инструмента перевода с открытым исходным кодом BartyCrouch, который интегрируется с двумя сторонними службами перевода.

⚠️ Обратите внимание, что если вы планируете применить этот подход к целевому приложению, которое вы будете отправлять пользователям, вы, вероятно, столкнетесь с той же проблемой, которая описана в подходе .xcconfig в этой статье NSHipster. Мой метод помогает только скрыть секреты от Git, вам понадобится дополнительная обфускация, если вы планируете отправлять пользователям.

Добавление файла ресурсов secrets.json

Во-первых, давайте добавим в наш проект файл secrets.json. Поскольку там будут соответствующие файлы secrets.json.sample и Secrets.swift, я сначала создаю папку Secrets, затем создаю пустой файл, который называю secrets.json, и добавляю простую структуру словаря JSON с двумя ключами:

Во-вторых, давайте удостоверимся, что мы не можем случайно зафиксировать этот файл, добавив secrets.json к нашему файлу .gitignore. Если в вашем проекте еще нет файла .gitignore, просто создайте его в корне репозитория, например запустив touch .gitignore. Если вы не видите файл в Finder, просто включите отображение скрытых файлов с помощью Cmd+Shift+.. Результат должен выглядеть примерно так:

Кстати: остальные записи выше в файле .gitignore скопированы из этого проекта сообщества GitHub, в частности из файлов macOS и Swift.

В-третьих, давайте продублируем наш файл secrets.json в Finder (Xcode не поддерживает дублирование файлов, насколько мне известно) и назовем его secrets.json.sample. Этот файл здесь для возврата в Git, чтобы другие, кто проверяет проект, могли легко продублировать его и удалить расширение .sample без необходимости искать, какие ключи действительно нужны. Секреты из этого файла, конечно, надо удалить, я заменю его какой-нибудь полезной подсказкой вроде <add secret here after duplicating this file & removing .sample ext>:

В-четвертых, нам нужно научить SwiftPM, где найти наш новый файл JSON, чтобы позже мы могли получить к нему доступ в коде. Для этого мы просто добавляем записи .copy к параметру resources нашей цели в файле манифеста. Достаточно указать относительный путь к целевой папке, в моем случае это BartyCrouchTranslatorTests. Результат выглядит примерно так:

Но с этой единственной записью resources мы получаем предупреждение от Xcode, потому что он находит наш файл secrets.json.sample в папке пакета, не зная, что с ним делать.

Это можно решить, изменив указанную выше запись с .copy("Secrets/secrets.json") на просто .copy("Secrets"), чтобы принимать все файлы в папке Secrets. Или, что я считаю более правильным, мы можем указать SwiftPM явно игнорировать файл .sample, добавив его в параметр exclude:

Загрузка секретов в код с помощью JSONDecoder

Теперь, когда у нас есть файл ресурсов secrets.json, давайте получим к нему доступ в Swift.

Во-первых, давайте создадим новый файл Swift с именем Secrets.swift с двумя нашими ключами в качестве свойств в простом struct, который соответствует Decodable:

Во-вторых, давайте реализуем некоторый код, который анализирует наш файл secrets.json. Я предпочитаю добавлять функциональность непосредственно в нашу новую структуру Secrets как функцию static:

Обратите внимание, что Bundle.module генерируется компилятором только в том случае, если у вас действительно есть хотя бы один ресурс, добавленный к вашей цели. Поэтому, если вы получаете ошибку компилятора, убедитесь, что вы добавили resources и что у вас действительно есть хотя бы один файл ресурсов в вашей цели, как мы сделали выше.

В-третьих, пришло время получить доступ к нашим секретам там, где они нам нужны, в моем случае в модульных тестах. Раньше у меня была такая строка, потому что я не хотел фиксировать свой ключ в общедоступном репозитории:

let subscriptionKey = ""

Теперь я могу просто загрузить свой ключ из файла secrets.json и получить к нему доступ следующим образом:

let subscriptionKey = try! Secrets.load().microsoftSubscriptionKey

Вот и все, я успешно получил доступ к своим секретам на своей машине, не регистрируя их в Git! Вы можете найти все изменения, которые я сделал в своем примере проекта, в этой единственной фиксации на GitHub.

Конечно, каждый, кто хочет запускать мои тесты с правильными ключами, должен с этого момента продублировать файл .sample и добавить правильные секреты. Следующим шагом для меня могло бы стать документирование этого в моем README.md или CONTRIBUTING.md. Точно так же вы можете сообщить об этом своей команде и даже поделиться нужным файлом secrets.json для проекта в безопасном месте, например, в менеджере паролей.

Дополнительно: настройка secrets.json на GitHub CI

Теперь, когда я загружаю секреты из файла JSON, я также хочу настроить конвейер GitHub CI для использования моих секретных ключей при выполнении тестов в CI.

Прежде чем начать, давайте добавим секреты в GitHub Actions, используя их функцию секретов (см. документацию здесь):

Для простоты в рабочем процессе GitHub Actions я просто использую команду echo и создаю файл со всем содержимым файла secrets.json по пути, где я ожидаю его, с помощью аргумента >> path/to/secrets.json. Доступ к секретам осуществляется через ${{ secrets.MICROSOFT_SUBSCRIPTION_KEY }}:

Теперь при каждом запуске CI файл secrets.json настраивается перед запуском тестов.

И поэтому мой CI также настроен на безопасный доступ к моим секретам без их утечки.

Want to Connect?
Follow me also on 👾 Twitch, on 🎬 YouTube and on 🐦 Twitter.