Разработка системы ролей и прав доступа мобильного приложения
Система прав доступа в мобильном приложении — это не просто if (user.role == "admin") перед кнопкой. Когда роли начинают множиться, появляются вложенные разрешения, временный доступ и гранулярные политики на отдельные ресурсы — примитивные проверки превращаются в разброшенный по кодовой базе хаос, который невозможно протестировать и легко сломать.
Модели управления доступом
Выбор модели — первое архитектурное решение. Три основных варианта:
RBAC (Role-Based Access Control) — роль привязана к набору разрешений. Пользователь получает роль, роль определяет доступ. Просто, понятно, работает для большинства B2B-приложений. Проблема: роли разрастаются. Начинается с admin, manager, viewer — заканчивается 40 ролями с частичным перекрытием.
ABAC (Attribute-Based Access Control) — доступ определяется атрибутами субъекта, ресурса и контекста. Правило типа allow if user.department == resource.department AND action == "read" AND time.hour < 18. Гибче RBAC, но сложнее в реализации и отладке.
ReBAC (Relationship-Based Access Control) — доступ через граф отношений. Используется в Google Zanzibar (и его open-source реализациях: OpenFGA, SpiceDB). Хорошо подходит для систем типа Google Drive, где "может редактировать папку, значит может редактировать всё внутри".
Для большинства корпоративных мобильных приложений достаточно RBAC с расширением через permission-флаги.
Реализация на клиенте
Главное правило: клиент ничему не доверяет сам себе. Проверки прав на мобильном — это UX-слой, не защита. Реальная защита — на backend. Но UX без правильной реализации прав приводит к тому, что пользователь видит кнопки, которые он нажать не может, или не видит функции, которые ему доступны.
На Android паттерн через ViewModel:
data class UserPermissions(
val canCreateOrder: Boolean,
val canApproveOrder: Boolean,
val canViewReports: Boolean,
val canManageUsers: Boolean,
val managedDepartments: List<String> // для гранулярного доступа
)
class OrderViewModel(
private val permissionsRepository: PermissionsRepository
) : ViewModel() {
val permissions = permissionsRepository.currentPermissions
.stateIn(viewModelScope, SharingStarted.Eagerly, UserPermissions())
fun createOrder(order: Order) {
check(permissions.value.canCreateOrder) { "Access denied" }
// ...
}
}
В Compose UI скрываем недоступные элементы, а не просто делаем disabled — это снижает когнитивную нагрузку:
val permissions by viewModel.permissions.collectAsState()
if (permissions.canCreateOrder) {
Button(onClick = { viewModel.createOrder(draft) }) {
Text("Создать заявку")
}
}
if (permissions.canApproveOrder) {
OutlinedButton(onClick = { viewModel.approveOrder(order) }) {
Text("Согласовать")
}
}
На iOS аналогичная логика через @Published в ObservableObject:
class PermissionsManager: ObservableObject {
@Published private(set) var permissions: UserPermissions = .empty
func refreshPermissions() async {
guard let token = authService.currentToken else { return }
permissions = try await permissionsAPI.fetch(token: token)
}
}
Хранение и синхронизация
Разрешения приходят с backend при логине и кэшируются локально. Критично: кэш должен инвалидироваться при изменении роли пользователя. Стандартный подход — TTL (15-30 минут) + принудительный рефреш при старте сессии.
Если роль может меняться в реальном времени (например, руководитель отзывает доступ), нужны WebSocket-уведомления или server-sent events с принудительным рефрешем разрешений без перелогина.
На Android — EncryptedSharedPreferences для хранения кэша разрешений (не SharedPreferences в открытом виде). На iOS — Keychain для токена, UserDefaults с кодированием через Codable для кэша прав.
Типичные проблемы
Гонка состояний при логине. Разрешения грузятся параллельно с навигацией. Пользователь успевает нажать кнопку до того, как permissions пришли. Решение: блокирующий экран загрузки пока permissions не получены, или оптимистичный UI с перепроверкой.
Hardcoded проверки в UI. if (role == "admin") разбросан по 30 экранам. Рефакторинг этого — боль. Решение с самого начала: централизованный PermissionsManager, UI читает только из него.
Нет логирования отказов. Backend отклоняет запрос с 403, мобильный показывает generic ошибку. В production непонятно, кто, что и почему не смог сделать. Нужен logging через Firebase Analytics или Sentry с permission_denied event и контекстом действия.
Этапы и сроки
Проектирование модели доступа, реализация PermissionsManager, интеграция с API, UI-адаптация, тестирование сценариев — 2-4 недели в зависимости от количества ролей и экранов. Если нужна реализация ReBAC с OpenFGA — к этому добавляется настройка backend-части. Стоимость рассчитывается индивидуально.







