Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use xcrun privacy instead of wix for location permission but keep using wix for rest #406

Merged
merged 16 commits into from
Nov 26, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The following tools and utilities are not mandatory, but could be used by the ap
- [Mobile Native Foundation](https://github.com/MobileNativeFoundation)
- [IDB](https://github.com/facebook/idb)
- [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
- For `contacts`, `camera`, `faceid`, `health`, `homekit`, `notifications`, `speech` and `userTracking` permissions

### Xcode and iOS versions

Expand Down
105 changes: 89 additions & 16 deletions lib/extensions/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import _ from 'lodash';
import { fs, util } from '@appium/support';
import { exec } from 'teen_process';
import path from 'path';
import B from 'bluebird';

const STATUS = Object.freeze({
UNSET: 'unset',
Expand All @@ -12,6 +13,17 @@ const STATUS = Object.freeze({
});

const WIX_SIM_UTILS = 'applesimutils';

// `location` permission does not work with WIX/applesimutils.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps, we should also report this as an issue to Wix?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found wix/AppleSimulatorUtils#111 in their repo. Let me link here

// Note that except for 'contacts', the Apple's privacy command sets
// permissions properly but it kills the app process while WIX/applesimutils does not.
// In the backward compatibility perspective,
// we'd like to keep the app process as possible.
const PERMISSIONS_APPLIED_VIA_SIMCTL = [
'location',
'location-always'
];

const SERVICES = Object.freeze({
calendar: 'kTCCServiceCalendar',
camera: 'kTCCServiceCamera',
Expand Down Expand Up @@ -82,22 +94,84 @@ async function execWix (args) {
/**
* Sets permissions for the given application
*
* @param {import('node-simctl').Simctl} simctl - node-simctl object.
* @param {string} udid - udid of the target simulator device.
* @param {string} bundleId - bundle identifier of the target application.
* @param {Object} permissionsMapping - An object, where keys ar service names
* and values are corresponding state values. See https://github.com/wix/AppleSimulatorUtils
* @param {Object} permissionsMapping - An object, where keys are service names
* and values are corresponding state values. Services listed in PERMISSIONS_APPLIED_VIA_SIMCTL
* will be set with `xcrun simctl privacy` command by Apple otherwise AppleSimulatorUtils by WIX.
* See the result of `xcrun simctl privacy` and https://github.com/wix/AppleSimulatorUtils
* for more details on available service names and statuses.
* Note that the `xcrun simctl privacy` command kill the app process.
* @throws {Error} If there was an error while changing permissions.
*/
async function setAccess (udid, bundleId, permissionsMapping) {
const permissionsArg = _.toPairs(permissionsMapping)
.map((x) => `${x[0]}=${formatStatus(x[1])}`)
.join(',');
return await execWix([
'--byId', udid,
'--bundle', bundleId,
'--setPermissions', permissionsArg,
]);
async function setAccess (simctl, udid, bundleId, permissionsMapping) {
const /** @type {Record<string, string>} */ wixPermissions = {};

const /** @type {string[]} */ grantPermissions = [];
const /** @type {string[]} */ revokePermissions = [];
const /** @type {string[]} */ resetPermissions = [];

for (const serviceName in permissionsMapping) {
if (!PERMISSIONS_APPLIED_VIA_SIMCTL.includes(serviceName)) {
wixPermissions[serviceName] = permissionsMapping[serviceName];
} else {
switch (permissionsMapping[serviceName]) {
case STATUS.YES:
grantPermissions.push(serviceName);
break;
case STATUS.NO:
revokePermissions.push(serviceName);
break;
case STATUS.UNSET:
resetPermissions.push(serviceName);
break;
default:
log.errorAndThrow(`${serviceName} does not support ${permissionsMapping[serviceName]}. Please specify 'yes', 'no' or 'unset'.`);
};
}
}

const /** @type {string[]} */ permissionPromises = [];

if (!_.isEmpty(grantPermissions)) {
log.debug(`Granting ${util.pluralize('permission', grantPermissions.length, false)} for ${bundleId}: ${grantPermissions}`);
for (const action of grantPermissions) {
permissionPromises.push(simctl.grantPermission(bundleId, action));
}
}

if (!_.isEmpty(revokePermissions)) {
log.debug(`Revoking ${util.pluralize('permission', revokePermissions.length, false)} for ${bundleId}: ${revokePermissions}`);
for (const action of revokePermissions) {
permissionPromises.push(simctl.revokePermission(bundleId, action));
}
}

if (!_.isEmpty(resetPermissions)) {
log.debug(`Resetting ${util.pluralize('permission', resetPermissions.length, false)} for ${bundleId}: ${resetPermissions}`);
for (const action of resetPermissions) {
permissionPromises.push(simctl.resetPermission(bundleId, action));
}
}

if (!_.isEmpty(permissionPromises)) {
await B.all(permissionPromises);
}

if (!_.isEmpty(wixPermissions)) {
log.debug(`Setting permissions for ${bundleId} wit ${WIX_SIM_UTILS} as ${JSON.stringify(wixPermissions)}`);
const permissionsArg = _.toPairs(wixPermissions)
.map((x) => `${x[0]}=${formatStatus(x[1])}`)
.join(',');
await execWix([
'--byId', udid,
'--bundle', bundleId,
'--setPermissions', permissionsArg,
]);
}

return true;
}

/**
Expand Down Expand Up @@ -144,9 +218,8 @@ async function getAccess (bundleId, serviceName, simDataRoot) {
const extensions = {};

/**
* Sets the particular permission to the application bundle. See
* https://github.com/wix/AppleSimulatorUtils for more details on
* the available service names and statuses.
* Sets the particular permission to the application bundle. See https://github.com/wix/AppleSimulatorUtils
* or `xcrun simctl privacy` for more details on the available service names and statuses.
*
* @param {string} bundleId - Application bundle identifier.
* @param {string} permission - Service name to be set.
Expand All @@ -163,13 +236,13 @@ extensions.setPermission = async function setPermission (bundleId, permission, v
* @param {string} bundleId - Application bundle identifier.
* @param {Object} permissionsMapping - A mapping where kays
* are service names and values are their corresponding status values.
* See https://github.com/wix/AppleSimulatorUtils
* See https://github.com/wix/AppleSimulatorUtils or `xcrun simctl privacy`
* for more details on available service names and statuses.
* @throws {Error} If there was an error while changing permissions.
*/
extensions.setPermissions = async function setPermissions (bundleId, permissionsMapping) {
log.debug(`Setting access for '${bundleId}': ${JSON.stringify(permissionsMapping, null, 2)}`);
await setAccess(this.udid, bundleId, permissionsMapping);
await setAccess(this.simctl, this.udid, bundleId, permissionsMapping);
};

/**
Expand Down
23 changes: 23 additions & 0 deletions test/functional/simulator-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,29 @@ describe('advanced features', function () {
});
});
});

describe('Permission', function () {
it('should set and get with simctrl privacy command', async function () {
// no exceptions
await expect(sim.setPermission('com.apple.Maps', 'location', 'yes')).not.to.be.rejected;
await expect(sim.setPermission('com.apple.Maps', 'location', 'no')).not.to.be.rejected;
await expect(sim.setPermission('com.apple.Maps', 'location', 'unset')).not.to.be.rejected;
await expect(sim.setPermission('com.apple.Maps', 'location', 'unsupported')).to.be.rejected;
});

it('should set and get with wix command', async function () {
await sim.setPermission('com.apple.Maps', 'contacts', 'yes');
await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('yes');
await sim.setPermission('com.apple.Maps', 'contacts', 'no');
await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('no');

// unset sets as 'no'
await sim.setPermission('com.apple.Maps', 'contacts', 'yes');
await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('yes');
await sim.setPermission('com.apple.Maps', 'contacts', 'unset');
await sim.getPermission('com.apple.Maps', 'contacts').should.eventually.eql('no');
});
});
});

describe(`multiple instances of ${OS_VERSION} simulator on Xcode9+`, function () {
Expand Down
Loading