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

Возможно, вы знаете о некоторых традиционных методах сокрытия секретов, таких как ключ 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:
- Зарегистрируйте файл
secrets.json.sampleв Git с ключами, но без значений - Позвольте разработчикам продублировать его, удалить расширение
.sampleи добавить значения. - Игнорировать файл
secrets.jsonчерез.gitignore, чтобы он никогда не возвращался - Предоставьте простой
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.