canListFirebaseApps method
Probe whether the active principal has firebase.apps.list on the
configured project — i.e. the permission needed by step 5.5
(Configure Firebase client wiring).
Why this exists: canEnableServices only verifies
serviceusage.services.enable which is granted by
roles/serviceusage.serviceUsageAdmin. A service account with that
role but no roles/firebase.admin will pass the existing IAM gate
in step 5.4 yet fail step 5.5 with firebase.apps.list
PERMISSION_DENIED. This probe closes that gap so the gate can grant
the missing firebase.admin role before step 5.5 runs.
Returns:
• true → the principal can list Firebase apps OR the call
failed for a reason that is NOT a missing IAM role
(e.g. the Firebase Management API is not enabled
yet — that's fine because step 5.4 will enable it
immediately after the gate passes). We err on the
side of "let the wizard proceed" so a brand-new
project doesn't get stuck in the gate trying to
auto-grant a role the SA already has.
• false → ONLY when the failure is unambiguously
PERMISSION_DENIED. This is what triggers the
auto-grant flow.
We use apps:list --json so the Firebase CLI emits a structured
{"status":"error", …} envelope instead of a generic "Failed to
list" stderr line, which lets _classifyFirebaseError tell the
difference between SERVICE_DISABLED (API not enabled — not an
IAM problem) and PERMISSION_DENIED (the actual IAM gap we want
to fix).
Implementation
Future<bool> canListFirebaseApps({String? account}) async {
final String? projectId = config.firebaseProjectId;
if (projectId == null) return false;
final ProcessResult r = await _runner.run(
'firebase',
<String>[
'apps:list',
'--project',
projectId,
'--json',
if (account != null && account.trim().isNotEmpty)
'--account=${account.trim()}',
],
environment: _authEnvironment,
workingDirectory: config.outputDir,
);
final Map<String, dynamic>? body = _parseFirebaseJson(r.stdout);
if (body != null && body['status'] == 'success') {
return true;
}
// Failure path: only treat PERMISSION_DENIED as "gate failed". If
// the firebase.googleapis.com API just isn't enabled yet (the
// SERVICE_DISABLED case), the gate should let the wizard continue
// — step 5.4 will enable the API, and the post-enable propagation
// poll will revalidate. Without this carve-out, a brand-new
// project where the API has never been enabled would force the
// gate into auto-grant for a role the SA may already have.
final String context = _collectFirebaseFailureContext(r);
final FirebaseFailureKind kind = _classifyFirebaseError(context);
if (kind == FirebaseFailureKind.permissionDenied) {
return false;
}
// serviceDisabled / transient / unknown → presume the SA has the
// role; the next step will surface any real failure.
return true;
}