feat: add validator to check for macOS app network entitlement (#2676)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -53,4 +53,8 @@ class Plist {
|
||||
? releaseVersion
|
||||
: '$releaseVersion+$buildNumber';
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
PropertyListSerialization.stringWithPropertyList(properties);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class Doctor {
|
||||
|
||||
/// Validators that verify shorebird will work on macOS.
|
||||
final List<Validator> macosCommandValidators = [
|
||||
// TODO(bryanoltman): ensure app has network capabilities
|
||||
MacosNetworkEntitlementValidator(),
|
||||
];
|
||||
|
||||
/// Validators that should run on all commands.
|
||||
@@ -34,6 +34,7 @@ class Doctor {
|
||||
ShorebirdVersionValidator(),
|
||||
ShorebirdFlutterValidator(),
|
||||
AndroidInternetPermissionValidator(),
|
||||
MacosNetworkEntitlementValidator(),
|
||||
ShorebirdYamlAssetValidator(),
|
||||
];
|
||||
|
||||
@@ -46,7 +47,7 @@ class Doctor {
|
||||
final allIssues = <ValidationIssue>[];
|
||||
final allFixableIssues = <ValidationIssue>[];
|
||||
|
||||
var numIssuesFixed = 0;
|
||||
var totalIssuesFixed = 0;
|
||||
for (final validator in validators) {
|
||||
if (!validator.canRunInCurrentContext()) {
|
||||
continue;
|
||||
@@ -76,13 +77,15 @@ class Doctor {
|
||||
// Re-run the validator to see if there are any remaining issues that
|
||||
// we couldn't fix.
|
||||
unresolvedIssues = await validator.validate();
|
||||
if (unresolvedIssues.isEmpty) {
|
||||
numIssuesFixed += issues.length - unresolvedIssues.length;
|
||||
final numIssuesFixed = issues.length - unresolvedIssues.length;
|
||||
if (numIssuesFixed > 0) {
|
||||
totalIssuesFixed += numIssuesFixed;
|
||||
final fixAppliedMessage =
|
||||
'''($numIssuesFixed fix${numIssuesFixed == 1 ? '' : 'es'} applied)''';
|
||||
validatorProgress.complete(
|
||||
'''${validator.description} ${green.wrap(fixAppliedMessage)}''',
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@@ -125,7 +128,7 @@ class Doctor {
|
||||
allIssues.addAll(unresolvedIssues);
|
||||
}
|
||||
|
||||
if (numIssuesFixed > 0) {
|
||||
if (totalIssuesFixed > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/archive_analysis/plist.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
|
||||
/// Checks that the macOS app has the network client entitlement. Without this
|
||||
/// entitlement, the app will not be able to make network requests, and
|
||||
/// Shorebird will not be able to check for patches.
|
||||
class MacosNetworkEntitlementValidator extends Validator {
|
||||
/// The plist key for the Outgoing Connections (Client) entitlement, which
|
||||
/// allows macOS apps to make network requests.
|
||||
static const networkClientEntitlementKey =
|
||||
'com.apple.security.network.client';
|
||||
|
||||
Directory? get _macosDirectory {
|
||||
final projectRoot = shorebirdEnv.getFlutterProjectRoot();
|
||||
if (projectRoot == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Directory(p.join(projectRoot.path, 'macos'));
|
||||
}
|
||||
|
||||
/// The entitlements plist file for the release build. This lives in
|
||||
/// project_root/macos/Runner/Release.entitlements, where "Runner" may have
|
||||
/// been renamed.
|
||||
File? get _releaseEntitlementsPlist {
|
||||
final entitlementParentCandidateDirectories =
|
||||
_macosDirectory!.listSync().whereType<Directory>();
|
||||
|
||||
for (final appDir in entitlementParentCandidateDirectories) {
|
||||
final entitlementsPlist = File(
|
||||
p.join(appDir.path, 'Release.entitlements'),
|
||||
);
|
||||
|
||||
if (entitlementsPlist.existsSync()) {
|
||||
return entitlementsPlist;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get description => 'macOS app has Outgoing Connections entitlement';
|
||||
|
||||
@override
|
||||
bool canRunInCurrentContext() =>
|
||||
_macosDirectory != null && _macosDirectory!.existsSync();
|
||||
|
||||
@override
|
||||
Future<List<ValidationIssue>> validate() async {
|
||||
if (_releaseEntitlementsPlist == null) {
|
||||
return [
|
||||
const ValidationIssue(
|
||||
severity: ValidationIssueSeverity.error,
|
||||
message: 'Unable to find a Release.entitlements file',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (!hasNetworkClientEntitlement(plistFile: _releaseEntitlementsPlist!)) {
|
||||
return [
|
||||
ValidationIssue(
|
||||
severity: ValidationIssueSeverity.error,
|
||||
message:
|
||||
'''${_releaseEntitlementsPlist!.path} is missing the Outgoing Connections ($networkClientEntitlementKey) entitlement.''',
|
||||
fix: () => addNetworkEntitlementToPlist(_releaseEntitlementsPlist!),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Whether the given entitlements plist file has the network client
|
||||
/// entitlement.
|
||||
@visibleForTesting
|
||||
static bool hasNetworkClientEntitlement({required File plistFile}) =>
|
||||
Plist(file: plistFile).properties[networkClientEntitlementKey] == true;
|
||||
|
||||
/// Adds the network client entitlement to the given entitlements plist file.
|
||||
@visibleForTesting
|
||||
static void addNetworkEntitlementToPlist(File entitlementsPlist) {
|
||||
final plist = Plist(file: entitlementsPlist);
|
||||
plist.properties[networkClientEntitlementKey] = true;
|
||||
entitlementsPlist.writeAsStringSync(plist.toString());
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
|
||||
export 'android_internet_permission_validator.dart';
|
||||
export 'flavor_validator.dart';
|
||||
export 'macos_network_entitlement_validator.dart';
|
||||
export 'shorebird_flutter_validator.dart';
|
||||
export 'shorebird_version_validator.dart';
|
||||
export 'shorebird_yaml_asset_validator.dart';
|
||||
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:scoped_deps/scoped_deps.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
group(MacosNetworkEntitlementValidator, () {
|
||||
const entitlementsPlistWithoutEntitlement = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
|
||||
const entitlementsPlistWithEntitlement = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
''';
|
||||
|
||||
late Directory projectRoot;
|
||||
late ShorebirdEnv shorebirdEnv;
|
||||
late MacosNetworkEntitlementValidator validator;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
File releaseEntitlementsFile() => File(
|
||||
p.join(projectRoot.path, 'macos', 'Runner', 'Release.entitlements'),
|
||||
);
|
||||
|
||||
void setUpProjectRoot({String? entitlements}) {
|
||||
Directory(
|
||||
p.join(projectRoot.path, 'macos', 'Runner'),
|
||||
).createSync(recursive: true);
|
||||
|
||||
if (entitlements != null) {
|
||||
releaseEntitlementsFile()
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync(entitlements);
|
||||
}
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
projectRoot = Directory.systemTemp.createTempSync();
|
||||
shorebirdEnv = MockShorebirdEnv();
|
||||
|
||||
when(() => shorebirdEnv.getFlutterProjectRoot()).thenReturn(projectRoot);
|
||||
|
||||
validator = MacosNetworkEntitlementValidator();
|
||||
});
|
||||
|
||||
group('description', () {
|
||||
test('returns the correct description', () {
|
||||
expect(
|
||||
runWithOverrides(() => validator.description),
|
||||
'macOS app has Outgoing Connections entitlement',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('canRunInCurrentContext', () {
|
||||
group('when macos directory exists', () {
|
||||
setUp(setUpProjectRoot);
|
||||
|
||||
test('returns true', () {
|
||||
expect(
|
||||
runWithOverrides(() => validator.canRunInCurrentContext()),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when macos directory does not exist', () {
|
||||
test('returns false', () {
|
||||
expect(
|
||||
runWithOverrides(() => validator.canRunInCurrentContext()),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('validate', () {
|
||||
group('when release entitlements plist does not exist', () {
|
||||
setUp(setUpProjectRoot);
|
||||
|
||||
test('returns a validation issue with no fix', () async {
|
||||
final issues = await runWithOverrides(() => validator.validate());
|
||||
expect(issues, hasLength(1));
|
||||
expect(
|
||||
issues[0],
|
||||
equals(
|
||||
const ValidationIssue(
|
||||
severity: ValidationIssueSeverity.error,
|
||||
message: 'Unable to find a Release.entitlements file',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when release entitlements plist exists', () {
|
||||
group('when network client entitlement is missing', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
|
||||
});
|
||||
|
||||
test('returns a validation issue with a fix', () async {
|
||||
final validationResults = await runWithOverrides(
|
||||
() => validator.validate(),
|
||||
);
|
||||
expect(validationResults, hasLength(1));
|
||||
final issue = validationResults[0];
|
||||
expect(issue.severity, ValidationIssueSeverity.error);
|
||||
expect(
|
||||
issue.message,
|
||||
contains(
|
||||
'''is missing the Outgoing Connections (com.apple.security.network.client) entitlement.''',
|
||||
),
|
||||
);
|
||||
expect(issue.fix, isNotNull);
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
runWithOverrides(() => issue.fix!());
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when network client entitlement is present', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
|
||||
});
|
||||
|
||||
test('returns an empty list', () async {
|
||||
expect(await runWithOverrides(() => validator.validate()), isEmpty);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('fix', () {
|
||||
group('when the network client entitlement is missing', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
|
||||
});
|
||||
|
||||
test('adds the network client entitlement to the entitlements plist',
|
||||
() {
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
MacosNetworkEntitlementValidator.addNetworkEntitlementToPlist(
|
||||
releaseEntitlementsFile(),
|
||||
);
|
||||
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when the network client entitlement is present', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
|
||||
});
|
||||
|
||||
test('does not modify the entitlements plist', () {
|
||||
final plistContents = releaseEntitlementsFile().readAsStringSync();
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
MacosNetworkEntitlementValidator.addNetworkEntitlementToPlist(
|
||||
releaseEntitlementsFile(),
|
||||
);
|
||||
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
final updatedPlistContents =
|
||||
releaseEntitlementsFile().readAsStringSync();
|
||||
expect(updatedPlistContents, plistContents);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('plistHasNetworkClientEntitlement', () {
|
||||
group('when entitlement is present', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithEntitlement);
|
||||
});
|
||||
|
||||
test('returns true', () {
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when entitlement is not present', () {
|
||||
setUp(() {
|
||||
setUpProjectRoot(entitlements: entitlementsPlistWithoutEntitlement);
|
||||
});
|
||||
|
||||
test('returns false', () {
|
||||
expect(
|
||||
MacosNetworkEntitlementValidator.hasNetworkClientEntitlement(
|
||||
plistFile: releaseEntitlementsFile(),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user