Feature/ios optional approov plist#32
Open
ivolz wants to merge 7 commits into
Open
Conversation
The iOS native module no longer raises ApproovPropsNotFound at launch when a bundled approov.plist is absent. With native approov.config-based init this made the properties file mandatory on iOS while Android treats approov.props as optional. It now logs and runs with default properties (leaving _props nil, which is safe: all consumers nil-guard and -valueForKey: on nil returns nil). A present-but-unreadable plist still raises (genuine misconfiguration). Also fixes a nil-pointer log when the file is absent. Reported by PropertyGuru (approov.config-only native integration crashed without an empty placeholder approov.plist).
initialize() reset the token/trace headers, substitution maps, exclusion regexes and other runtime state when the config changed, but never reset the custom service mutator. A mutator installed via setServiceMutator (or the JS setServiceMutatorType wrapper) therefore persisted across an initialization boundary — violating root TESTING_REQUIREMENTS.md section 2 "Service Mutator Reset" and diverging from approov-service-okhttp, which resets the mutator to the default on init. Reset the mutator to a fresh ApproovDefaultMessageSigning (the React Native default, which performs HTTP message signing) inside the config-changed reset block, rather than the no-signing ApproovServiceMutator.DEFAULT that would silently disable default signing on every re-init. Same-config re-inits (React remounts / StrictMode / Fast Refresh) intentionally preserve runtime state and are unaffected. Adds a regression test (initializeWithDifferentConfigResetsCustomServiceMutator). Pre-existing defect, independent of the prebuilt-mutator feature work. iOS needs the equivalent reset when the Android changes are ported.
The ObjC initialize() reset block cleared token/trace headers, substitution maps and exclusion regexes on a config change but never reset the active service mutator, so a custom mutator installed via ApproovServiceMutatorBridge persisted across an initialization boundary — violating root TESTING_REQUIREMENTS.md section 2 "Service Mutator Reset". This is the iOS counterpart of the Android fix (commit 48ee5b6). Adds ApproovServiceMutatorBridge.resetToDefault() (an @objc helper, since the Swift-typed serviceMutator cannot be assigned from Objective-C) and calls it in the config-changed reset block, restoring a fresh ApproovDefaultMessageSigning (the RN default, which performs message signing). Same-config re-inits intentionally preserve state and are unaffected.
Add ApproovService.setServiceMutatorType(mask, { sign }) plus the ApproovService.ReturnDecision status flags and ApproovService.MutatorPreset presets, so an app can choose the per-status proceed/block policy — and whether the outbound request is HTTP Message Signed — entirely from JavaScript, with no native mutator code.
Android: PolicyMutator (bitmask-driven, composes ApproovDefaultMessageSigning) + setServiceMutatorType @ReactMethod. iOS: Swift PolicyMutator + ApproovServiceMutatorBridge.setPolicyMutator + RCT_EXPORT_METHOD setServiceMutatorType, behaviour identical to Android — a not-in-mask failure status blocks the request via ApproovServiceError.permanentError, the same hard failure as Android's IOException. JS: ReturnDecision flags + MutatorPreset presets (DEFAULT, ALWAYS_PROCEED, PROCEED_IF_UNAVAILABLE, PROCEED_DEV_CLEARTEXT). Message signing preserved by default; { sign: false } opts out.
Docs: USAGE.md, REFERENCE.md, CHANGELOG (3.5.16), including the security caveat that proceeding on MITM_DETECTED / REJECTED disables that protection. Tests: Android PolicyMutatorTest (JVM); iOS PolicyMutatorTests (Swift) + run_policy_mutator_tests.sh. PROCEED_DEV_CLEARTEXT is the supported opt-in for the Metro dev BAD_URL symptom (#30).
Contributor
There was a problem hiding this comment.
Pull request overview
This PR (targeting release 3.5.16) extends the React Native Approov service layer with a JS-selectable request policy mutator (bitmask + presets), fixes service-mutator reset behavior across config-change re-initializations, and makes the iOS bundled approov.plist optional to avoid launch crashes for config-only native integrations.
Changes:
- Add
ApproovService.setServiceMutatorType(mask, { sign })plusReturnDecisionbit flags andMutatorPresetpresets, backed by new nativePolicyMutatorimplementations on Android and iOS. - Reset the active service mutator back to the built-in message-signing default when
initialize()is called with a genuinely different config (Android + iOS). - Make iOS
approov.plistoptional (missing file logs + defaults; unreadable file still raises), and add/extend native/unit test coverage and documentation.
Reviewed changes
Copilot reviewed 20 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| USAGE.md | Documents JS-driven policy selection (presets, raw bitmask, signing option) and re-init reset semantics. |
| REFERENCE.md | Adds API reference for setServiceMutatorType, ReturnDecision, and MutatorPreset. |
| CHANGELOG.md | Adds 3.5.16 release notes for the new policy API and the two fixes. |
| package.json | Bumps package version to 3.5.16. |
| package-lock.json | Updates lockfile version fields to 3.5.16. |
| index.js | Adds Proxy wrapper for setServiceMutatorType and exports ReturnDecision/MutatorPreset. |
| index.d.ts | Types the new JS API and the added constants. |
| android/src/main/java/io/approov/reactnative/ApproovService.java | Adds native setServiceMutatorType method and resets mutator on config-change re-init. |
| android/src/main/java/io/approov/reactnative/PolicyMutator.java | New Android native mutator implementing proceed/forward/block policy + optional signing. |
| android/src/test/java/io/approov/reactnative/PolicyMutatorTest.java | New unit tests for Android PolicyMutator decision logic and signing behavior. |
| android/src/test/java/io/approov/reactnative/ApproovServiceRegressionTest.java | Adds regression test to ensure mutator resets on config-change re-init. |
| ios/ApproovProps.m | Makes missing approov.plist optional (log + defaults) instead of raising at launch. |
| ios/ApproovService.m | Resets mutator on config-change re-init and adds exported setServiceMutatorType bridge method. |
| ios/ApproovServiceMutatorBridge.swift | Adds ObjC-callable helpers to install PolicyMutator and reset to default signer. |
| ios/ApproovURLSession/PolicyMutator.swift | New Swift PolicyMutator implementing bitmask decision policy + optional signing via composition. |
| tests/ios/swift/TestSupport/ApproovStub.swift | Extends Swift stub statuses to cover DISABLED and INTERNAL_ERROR. |
| tests/ios/swift/PolicyMutatorTests.swift | New Swift test runner validating bit assignments, blocking semantics, signing delegation, and bridge behavior. |
| tests/ios/run_policy_mutator_tests.sh | New script compiling/running the Swift PolicyMutator test runner via swiftc. |
| tests/ios/run_message_signing_tests.sh | Includes PolicyMutator.swift in the message signing swiftc compilation set. |
| tests/ios/run_native_tests.sh | Adds the Swift PolicyMutator suite to the iOS native test runner. |
| tests/ios/native/TestSupport/ApproovServiceMutatorBridgeStub.m | Adds no-op ObjC stub selectors for new Swift bridge helpers to keep native suites linking. |
| tests/ios/native/TestSupport/approov_service_react_native-Swift.h | Adds stub header declarations for setPolicyMutator and resetToDefault. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The proceed bitmask is bridged from JS as a double and was narrowed to int (Android) / NSInteger (iOS) with an unchecked cast. A fractional, NaN/Infinity, or out-of-32-bit-range value was silently truncated or coerced and could install a proceed policy other than the caller's intent (security-relevant: the mask decides which failure statuses may proceed). Reject such masks before applying the mutator on both platforms. Adds Android and iOS native regression tests.
PolicyMutator's block error interpolated the Swift enum case via \(status); Approov.string(from:) yields the canonical MITM_DETECTED-style name, matching the rest of the iOS layer and Android's enum toString(). Also reword the MITM_DETECTED/REJECTED mask security warning in REFERENCE.md, USAGE.md and CHANGELOG.md to state the request proceeds without proof of attestation (no valid Approov token). (Copilot review, PR #32.)
index.js: options = {} only defaults on undefined; an explicit null
(setServiceMutatorType(mask, null)) threw on options.sign. Treat null
like no options via (options || {}), so sign defaults to true.
ios/ApproovProps.m: correct the optional-props comment - the accessor
is -objectForKey: sent to the nil _props dictionary, not -valueForKey:.
(Copilot review, PR #32.)
Comment on lines
+323
to
+347
| public void initializeWithDifferentConfigResetsCustomServiceMutator() { | ||
| ApproovService service = newService(); | ||
|
|
||
| Promise firstInit = mock(Promise.class); | ||
| service.initialize("config-one", null, firstInit); | ||
| verify(firstInit, timeout(2000)).resolve(null); | ||
|
|
||
| // Install a custom, non-signing mutator (as an app would via setServiceMutator, | ||
| // or the JS setServiceMutatorType wrapper). | ||
| ApproovServiceMutator custom = ApproovServiceMutator.DEFAULT; | ||
| ApproovService.setServiceMutator(custom); | ||
| assertSame(custom, ApproovService.getServiceMutator()); | ||
|
|
||
| // A genuinely different config must reset the mutator so a custom override does | ||
| // not persist across an initialization boundary (root TESTING_REQUIREMENTS.md | ||
| // section 2, "Service Mutator Reset"). | ||
| Promise secondInit = mock(Promise.class); | ||
| service.initialize("config-two", null, secondInit); | ||
| verify(secondInit, timeout(2000)).resolve(null); | ||
|
|
||
| ApproovServiceMutator afterReset = ApproovService.getServiceMutator(); | ||
| assertNotSame("custom mutator must not persist across a config change", custom, afterReset); | ||
| assertTrue("re-init must restore the default message-signing mutator", | ||
| afterReset instanceof ApproovDefaultMessageSigning); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships as 3.5.16 (last published: 3.5.15). Three related changes to the React Native service layer, on both Android and iOS:
setServiceMutatorType), no native mutator code.approov.plistis now optional (no launch crash with a config-only native integration).Feature —
ApproovService.setServiceMutatorType(mask, { sign })ApproovService.ReturnDecision(one bit per Approov failure status) +ApproovService.MutatorPresetpresets (DEFAULT,ALWAYS_PROCEED,PROCEED_IF_UNAVAILABLE,PROCEED_DEV_CLEARTEXT).SUCCESS/UNKNOWN_URL/UNPROTECTED_URLalways proceed and are not maskable.{ sign: false }opts out.PolicyMutatoron each platform (Android Java, iOS Swift), composing the default message signer. Behaviour is identical across platforms — a blocked status is a hardfetch()failure on both (AndroidIOException, iOSNSError).ApproovServiceMutator).MITM_DETECTEDorREJECTEDin the mask disables the protection those statuses provide — use presets by default; reach for the raw bitmask deliberately.PROCEED_DEV_CLEARTEXTis the supported dev-only opt-in for the Metro cleartextBAD_URLsymptom (BAD_URL token status crashes network requests instead of being handled gracefully #30).Fix — service mutator reset on re-initialization (Android + iOS)
initialize()cleared the runtime config on a config change but not the active service mutator, so a mutator installed viasetServiceMutator/setServiceMutatorTypepersisted across an initialization boundary. It now resets to the built-in message-signing default on both platforms whenever the config changes (root TESTING_REQUIREMENTS §2, "Service Mutator Reset"). Same-config re-inits still preserve state.Fix — iOS
approov.plistoptionalWith native
approov.configinit, a missing bundledapproov.plistno longer raisesApproovPropsNotFoundat launch — it logs and runs with defaults, matching Android's optionalapproov.props. A present-but-unreadable plist still raises. (Reported by PropertyGuru.)Versioning
Patch 3.5.16 — all three changes ship together (last published 3.5.15).
package.json, podspec (s.version = package["version"]) andPackage.swift(git-tag) are consistent; the release tag will be3.5.16.Testing
PolicyMutatorTest+ApproovServiceRegressionTest(mutator-reset regression) — full unit suite 55 tests, all green.run_policy_mutator_tests.sh(Swift),run_message_signing_tests.sh,run_legacy_native_tests.sh— all pass.run_mini_sdk_native_tests.sh(needspod install+ the externalcore-service-layers-testing/mini-sdk/ioscheckout) — to run in CI before release.Related
BAD_URL;PROCEED_DEV_CLEARTEXTis the supported opt-in.@ReactMethoddropped under New Architecture" report; disproven on-device).