Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Source/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@ objc_library(
":Pinning",
":SNTCELFallbackRule",
":SNTCommonEnums",
":SNTConfigBundle",
":SNTExportConfiguration",
":SNTLiteDetector",
":SNTLogging",
Expand Down Expand Up @@ -1094,6 +1095,7 @@ santa_unit_test(
srcs = ["SNTConfiguratorTest.mm"],
deps = [
":SNTCommonEnums",
":SNTConfigBundle",
":SNTConfigurator",
"@OCMock",
],
Expand Down
14 changes: 14 additions & 0 deletions Source/common/SNTConfigurator.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#import <Foundation/Foundation.h>

#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigBundle.h"

@class SNTCELFallbackRule;
@class SNTExportConfiguration;
Expand Down Expand Up @@ -1152,6 +1153,19 @@ extern NSString* _Nonnull const kEnableMenuItemUserOverride;
///
- (void)clearSyncState;

///
/// Atomically replace the persisted sync state with a new dictionary
/// containing only the existing `PushTokenChain` (if present) plus any
/// non-nil values from `bundle`. Performs a single `saveSyncStateToDisk`
/// via `writeToFile:atomically:YES`.
///
/// Used by the daemon's `replaceSyncSettings:reply:` XPC handler when a
/// clean sync's postflight is committing settings under the SNT-357
/// semantics. Workshop-managed keys not present in `bundle` are
/// cleared from disk by this call.
///
- (void)replaceSyncStateWithBundle:(nonnull SNTConfigBundle*)bundle;

///
/// Validate the configuration profile.
///
Expand Down
138 changes: 134 additions & 4 deletions Source/common/SNTConfigurator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1959,11 +1959,17 @@ - (NSDictionary*)extraMetricLabels {
/// immediately after the call completes.
///
- (void)updateSyncStateForKey:(NSString*)key value:(id)value {
[self updateSyncStateForKey:key value:value writeToDisk:YES];
}

- (void)updateSyncStateForKey:(NSString*)key value:(id)value writeToDisk:(BOOL)writeToDisk {
void (^block)(void) = ^{
NSMutableDictionary* syncState = self.syncState.mutableCopy;
syncState[key] = value;
self.syncState = syncState;
[self saveSyncStateToDisk];
if (writeToDisk) {
[self saveSyncStateToDisk];
}
};
// Avoid deadlocks by directly calling the block when already running on the
// main thread.
Expand Down Expand Up @@ -2061,9 +2067,133 @@ - (void)saveSyncStateToDisk {

- (void)clearSyncState {
self.syncState = [NSMutableDictionary dictionary];
// TODO: Start a timer to flush the state to disk. On startup, Santa should
// check for the presence of the state file and, if no SyncBaseURL is
// configured, start the timer to clear sync state and flush to disk.
[[NSFileManager defaultManager] removeItemAtPath:self.syncStateFilePath error:NULL];
}

- (void)replaceSyncStateWithBundle:(SNTConfigBundle*)bundle {
void (^block)(void) = ^{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: we should add an "is authorized" check:

  if (!self.syncStateAccessAuthorizerBlock()) {
    return;
  }

NSMutableDictionary* newSyncState = [NSMutableDictionary dictionary];

NSArray* preservedChain = self.syncState[kPushTokenChainKey];
if ([preservedChain isKindOfClass:[NSArray class]] && preservedChain.count > 0) {
newSyncState[kPushTokenChainKey] = preservedChain;
}
Comment thread
sharvilshah marked this conversation as resolved.
Outdated

self.syncState = newSyncState;

[self applySyncBundleSettersDeferringDiskWrites:bundle];

[self saveSyncStateToDisk];
};

if ([NSThread isMainThread]) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}

- (void)applySyncBundleSettersDeferringDiskWrites:(SNTConfigBundle*)bundle {
[bundle clientMode:^(SNTClientMode m) {
[self updateSyncStateForKey:kClientModeKey value:@(m) writeToDisk:NO];
}];
[bundle syncType:^(SNTSyncType val) {
[self updateSyncStateForKey:kSyncTypeRequired value:@(val) writeToDisk:NO];
}];
[bundle allowlistRegex:^(NSString* val) {
[self updateSyncStateForKey:kAllowedPathRegexKey
value:[NSRegularExpression regularExpressionWithPattern:val
options:0
error:NULL]
writeToDisk:NO];
}];
[bundle blocklistRegex:^(NSString* val) {
[self updateSyncStateForKey:kBlockedPathRegexKey
value:[NSRegularExpression regularExpressionWithPattern:val
options:0
error:NULL]
writeToDisk:NO];
}];
[bundle removableMediaAction:^(NSString* val) {
[self updateSyncStateForKey:kRemovableMediaActionKey value:val writeToDisk:NO];
}];
[bundle removableMediaRemountFlags:^(NSArray<NSString*>* val) {
[self updateSyncStateForKey:kRemovableMediaRemountFlagsKey value:val writeToDisk:NO];
}];
[bundle encryptedRemovableMediaAction:^(NSString* val) {
[self updateSyncStateForKey:kEncryptedRemovableMediaActionKey value:val writeToDisk:NO];
}];
[bundle encryptedRemovableMediaRemountFlags:^(NSArray<NSString*>* val) {
[self updateSyncStateForKey:kEncryptedRemovableMediaRemountFlagsKey value:val writeToDisk:NO];
}];
[bundle blockNetworkMount:^(BOOL val) {
[self updateSyncStateForKey:kBlockNetworkMountKey value:@(val) writeToDisk:NO];
}];
[bundle bannedNetworkMountBlockMessage:^(NSString* val) {
[self updateSyncStateForKey:kBannedNetworkMountBlockMessage value:val writeToDisk:NO];
}];
[bundle allowedNetworkMountHosts:^(NSArray<NSString*>* val) {
[self updateSyncStateForKey:kAllowedNetworkMountHosts value:val writeToDisk:NO];
}];
[bundle enableBundles:^(BOOL val) {
[self updateSyncStateForKey:kEnableBundlesKey value:@(val) writeToDisk:NO];
}];
[bundle enableTransitiveRules:^(BOOL val) {
[self updateSyncStateForKey:kEnableTransitiveRulesKey value:@(val) writeToDisk:NO];
}];
[bundle enableAllEventUpload:^(BOOL val) {
[self updateSyncStateForKey:kEnableAllEventUploadKey value:@(val) writeToDisk:NO];
}];
[bundle disableUnknownEventUpload:^(BOOL val) {
[self updateSyncStateForKey:kDisableUnknownEventUploadKey value:@(val) writeToDisk:NO];
}];
[bundle overrideFileAccessAction:^(NSString* val) {
[self updateSyncStateForKey:kOverrideFileAccessActionKey value:val writeToDisk:NO];
}];
[bundle exportConfiguration:^(SNTExportConfiguration* val) {
[self updateSyncStateForKey:kExportConfigurationKey value:[val serialize] writeToDisk:NO];
}];
[bundle fullSyncLastSuccess:^(NSDate* val) {
[self updateSyncStateForKey:kFullSyncLastSuccess value:val writeToDisk:NO];
}];
[bundle ruleSyncLastSuccess:^(NSDate* val) {
[self updateSyncStateForKey:kRuleSyncLastSuccess value:val writeToDisk:NO];
Comment thread
sharvilshah marked this conversation as resolved.
Outdated
}];
[bundle modeTransition:^(SNTModeTransition* val) {
[self updateSyncStateForKey:kModeTransitionKey value:[val serialize] writeToDisk:NO];
}];
[bundle networkExtensionSettings:^(SNTSyncNetworkExtensionSettings* val) {
[self updateSyncStateForKey:kNetworkExtensionSettingsKey value:[val serialize] writeToDisk:NO];
}];
[bundle pushTokenChain:^(NSArray<NSString*>* val) {
[self updateSyncStateForKey:kPushTokenChainKey value:val writeToDisk:NO];
}];
[bundle telemetryFilterExpressions:^(NSArray<NSString*>* val) {
[self updateSyncStateForKey:kTelemetryFilterExpressionsKey value:val writeToDisk:NO];
}];
[bundle celFallbackRules:^(NSArray<SNTCELFallbackRule*>* val) {
[self updateSyncStateForKey:kCELFallbackRulesKey
value:[SNTCELFallbackRule serializeArray:val]
writeToDisk:NO];
}];
[bundle eventDetailURL:^(NSString* val) {
[self updateSyncStateForKey:kEventDetailURLKey value:val writeToDisk:NO];
}];
[bundle eventDetailText:^(NSString* val) {
[self updateSyncStateForKey:kEventDetailTextKey value:val writeToDisk:NO];
}];
[bundle fileAccessEventDetailURL:^(NSString* val) {
[self updateSyncStateForKey:kFileAccessEventDetailURLKey value:val writeToDisk:NO];
}];
[bundle fileAccessEventDetailText:^(NSString* val) {
[self updateSyncStateForKey:kFileAccessEventDetailTextKey value:val writeToDisk:NO];
}];
[bundle fullSyncInterval:^(NSUInteger val) {
[self updateSyncStateForKey:kFullSyncInterval value:val ? @(val) : nil writeToDisk:NO];
}];
[bundle pushNotificationsFullSyncInterval:^(NSUInteger val) {
[self updateSyncStateForKey:kFCMFullSyncInterval value:val ? @(val) : nil writeToDisk:NO];
}];
}

- (NSArray*)entitlementsPrefixFilter {
Expand Down
156 changes: 156 additions & 0 deletions Source/common/SNTConfiguratorTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <XCTest/XCTest.h>

#import "Source/common/SNTCommonEnums.h"
#import "Source/common/SNTConfigBundle.h"
#import "Source/common/SNTConfigurator.h"

typedef BOOL (^StateFileAccessAuthorizer)(void);
Expand All @@ -32,6 +33,15 @@ - (instancetype)initWithSyncStateFile:(NSString*)syncStateFilePath
@property NSMutableDictionary* syncState;
@end

// Expose SNTConfigBundle's writable properties for test setup. These properties
// are declared as a class extension in SNTConfigBundle.mm; redeclaring them as
// a category here gives the test typed setter access.
@interface SNTConfigBundle (ConfigBundleCreator)
@property NSNumber* clientMode;
@property NSDate* fullSyncLastSuccess;
@property NSArray<NSString*>* pushTokenChain;
@end

@interface SNTConfiguratorTest : XCTestCase
@property NSFileManager* fileMgr;
@property NSString* testDir;
Expand Down Expand Up @@ -285,4 +295,150 @@ - (void)testAllowDelegatedSignalsOverride {
XCTAssertFalse(sut.allowDelegatedSignals);
}

#pragma mark - replaceSyncStateWithBundle: tests

- (SNTConfigurator*)configuratorWithSyncState:(NSDictionary*)initialState
plistPath:(NSString*)plistPath {
if (initialState) {
XCTAssertTrue([initialState writeToFile:plistPath atomically:YES]);
}
return [[SNTConfigurator alloc] initWithSyncStateFile:plistPath
stateFile:@"/does/not/need/to/exist"
oldStateFile:@"/does/not/need/to/exist"
syncStateAccessAuthorizer:^{
return YES;
}
stateAccessAuthorizer:^BOOL {
return NO;
}];
}

- (void)testReplaceSyncStateWithBundleClearsWorkshopKeysPreservingPushTokenChain {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-replace-state.plist", self.testDir];
NSArray<NSString*>* chain = @[ @"issuer-jwt-value", @"device-jwt-value" ];

SNTConfigurator* cfg = [self configuratorWithSyncState:@{
@"ClientMode" : @(SNTClientModeLockdown),
@"AllowedPathRegex" : @"old-allow-pattern",
@"PushTokenChain" : chain,
}
plistPath:plistPath];

SNTConfigBundle* bundle = [[SNTConfigBundle alloc] init];
bundle.clientMode = @(SNTClientModeMonitor);
bundle.fullSyncLastSuccess = [NSDate date];

[cfg replaceSyncStateWithBundle:bundle];

NSDictionary* onDisk = [NSDictionary dictionaryWithContentsOfFile:plistPath];
XCTAssertEqualObjects(onDisk[@"PushTokenChain"], chain);
XCTAssertEqual([onDisk[@"ClientMode"] integerValue], SNTClientModeMonitor);
XCTAssertNotNil(onDisk[@"FullSyncLastSuccess"]);
XCTAssertNil(onDisk[@"AllowedPathRegex"]);

XCTAssertTrue([self.fileMgr removeItemAtPath:plistPath error:nil]);
}

- (void)testReplaceSyncStateWithBundleNoExistingChainProducesNoChainOnDisk {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-replace-state.plist", self.testDir];

SNTConfigurator* cfg = [self configuratorWithSyncState:@{
@"ClientMode" : @(SNTClientModeLockdown),
}
plistPath:plistPath];

SNTConfigBundle* bundle = [[SNTConfigBundle alloc] init];
bundle.clientMode = @(SNTClientModeMonitor);

[cfg replaceSyncStateWithBundle:bundle];

NSDictionary* onDisk = [NSDictionary dictionaryWithContentsOfFile:plistPath];
XCTAssertNil(onDisk[@"PushTokenChain"]);
XCTAssertEqual([onDisk[@"ClientMode"] integerValue], SNTClientModeMonitor);

XCTAssertTrue([self.fileMgr removeItemAtPath:plistPath error:nil]);
}

- (void)testReplaceSyncStateWithBundleEmptyBundleLeavesOnlyChain {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-replace-state.plist", self.testDir];
NSArray<NSString*>* chain = @[ @"issuer", @"device" ];

SNTConfigurator* cfg = [self configuratorWithSyncState:@{
@"ClientMode" : @(SNTClientModeLockdown),
@"PushTokenChain" : chain,
}
plistPath:plistPath];

SNTConfigBundle* bundle = [[SNTConfigBundle alloc] init];

[cfg replaceSyncStateWithBundle:bundle];

NSDictionary* onDisk = [NSDictionary dictionaryWithContentsOfFile:plistPath];
XCTAssertEqualObjects(onDisk[@"PushTokenChain"], chain);
XCTAssertEqual(onDisk.count, (NSUInteger)1);

XCTAssertTrue([self.fileMgr removeItemAtPath:plistPath error:nil]);
}

- (void)testReplaceSyncStateWithBundleBundleChainOverridesPreserved {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-replace-state.plist", self.testDir];
NSArray<NSString*>* staleChain = @[ @"old-issuer", @"old-device" ];
NSArray<NSString*>* freshChain = @[ @"new-issuer", @"new-device" ];

SNTConfigurator* cfg = [self configuratorWithSyncState:@{
@"PushTokenChain" : staleChain,
}
plistPath:plistPath];

SNTConfigBundle* bundle = [[SNTConfigBundle alloc] init];
bundle.pushTokenChain = freshChain;

[cfg replaceSyncStateWithBundle:bundle];

NSDictionary* onDisk = [NSDictionary dictionaryWithContentsOfFile:plistPath];
XCTAssertEqualObjects(onDisk[@"PushTokenChain"], freshChain);

XCTAssertTrue([self.fileMgr removeItemAtPath:plistPath error:nil]);
}

#pragma mark - clearSyncState tests (Bug 2)

- (void)testClearSyncStateRemovesDiskFile {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-clear-state.plist", self.testDir];
NSDictionary* contents = @{@"ClientMode" : @(SNTClientModeLockdown)};
XCTAssertTrue([contents writeToFile:plistPath atomically:YES]);
XCTAssertTrue([self.fileMgr fileExistsAtPath:plistPath]);

SNTConfigurator* cfg = [self configuratorWithSyncState:nil plistPath:plistPath];
cfg.syncState = contents.mutableCopy;

[cfg clearSyncState];

XCTAssertEqual(cfg.syncState.count, (NSUInteger)0);
XCTAssertFalse([self.fileMgr fileExistsAtPath:plistPath]);
}

- (void)testClearSyncStateNoFileDoesNotError {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-clear-state.plist", self.testDir];
XCTAssertFalse([self.fileMgr fileExistsAtPath:plistPath]);

SNTConfigurator* cfg = [self configuratorWithSyncState:nil plistPath:plistPath];

XCTAssertNoThrow([cfg clearSyncState]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:plistPath]);
}

- (void)testClearSyncStateIdempotent {
NSString* plistPath = [NSString stringWithFormat:@"%@/test-clear-state.plist", self.testDir];
NSDictionary* contents = @{@"ClientMode" : @(SNTClientModeLockdown)};
XCTAssertTrue([contents writeToFile:plistPath atomically:YES]);

SNTConfigurator* cfg = [self configuratorWithSyncState:nil plistPath:plistPath];
cfg.syncState = contents.mutableCopy;

[cfg clearSyncState];
XCTAssertNoThrow([cfg clearSyncState]);
XCTAssertFalse([self.fileMgr fileExistsAtPath:plistPath]);
}

@end
8 changes: 8 additions & 0 deletions Source/common/SNTXPCControlInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ typedef NS_ENUM(NSInteger, SNTRuleAddSource) {
///
- (void)updateSyncSettings:(SNTConfigBundle*)result reply:(void (^)(void))reply;

///
/// Atomically replace the persisted sync state with the contents of `bundle`,
/// preserving only `PushTokenChain` from the previous state. Used by the
/// sync service's postflight stage when a clean sync (Clean or CleanAll)
/// resolves and the user did not pass `--keep-old-settings`.
///
- (void)replaceSyncSettings:(SNTConfigBundle*)bundle reply:(void (^)(void))reply;

///
/// Syncd Ops
///
Expand Down
1 change: 1 addition & 0 deletions Source/common/SNTXPCSyncServiceInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
//
- (void)syncWithLogListener:(NSXPCListenerEndpoint*)logListener
syncType:(SNTSyncType)syncType
keepOldSettings:(BOOL)keepOldSettings
reply:(void (^)(SNTSyncStatusType))reply;

// Publish metrics to the sync server. The metrics dictionary is the output of SNTMetricSet export.
Expand Down
Loading
Loading