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

Check for entitlement in DBP agent #2802

Merged
merged 17 commits into from
May 23, 2024
13 changes: 8 additions & 5 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

private var networkProtectionSubscriptionEventHandler: NetworkProtectionSubscriptionEventHandler?
#if DBP
private var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler?
private lazy var dataBrokerProtectionSubscriptionEventHandler: DataBrokerProtectionSubscriptionEventHandler = {
let authManager = DataBrokerAuthenticationManagerBuilder.buildAuthenticationManager(subscriptionManager: subscriptionManager)
return DataBrokerProtectionSubscriptionEventHandler(featureDisabler: DataBrokerProtectionFeatureDisabler(),
authenticationManager: authManager,
pixelHandler: DataBrokerProtectionPixelsHandler())
}()

#endif

private var didFinishLaunching = false
Expand Down Expand Up @@ -216,9 +222,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
networkProtectionSubscriptionEventHandler = NetworkProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager,
tunnelController: tunnelController,
vpnUninstaller: vpnUninstaller)
#if DBP
dataBrokerProtectionSubscriptionEventHandler = DataBrokerProtectionSubscriptionEventHandler(subscriptionManager: subscriptionManager)
#endif
}

// swiftlint:disable:next function_body_length
Expand Down Expand Up @@ -310,7 +313,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
UNUserNotificationCenter.current().delegate = self

#if DBP
dataBrokerProtectionSubscriptionEventHandler?.registerForSubscriptionAccountManagerEvents()
dataBrokerProtectionSubscriptionEventHandler.registerForSubscriptionAccountManagerEvents()
#endif

#if DBP
Expand Down
6 changes: 5 additions & 1 deletion DuckDuckGo/DBP/DataBrokerProtectionPixelsHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.homeViewShowWebUI,
.homeViewShowBadPathError,
.homeViewCTAMoveApplicationClicked,
.homeViewCTAGrantPermissionClicked:
.homeViewCTAGrantPermissionClicked,

.entitlementCheckValid,
.entitlementCheckInvalid,
.entitlementCheckError:
PixelKit.fire(event, frequency: .dailyAndCount)
}
}
Expand Down
43 changes: 35 additions & 8 deletions DuckDuckGo/DBP/DataBrokerProtectionSubscriptionEventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,55 @@ import Foundation
import Subscription
import DataBrokerProtection
import PixelKit
import Common

final class DataBrokerProtectionSubscriptionEventHandler {

private let subscriptionManager: SubscriptionManaging
private let authRepository: AuthenticationRepository
private let authenticationManager: DataBrokerProtectionAuthenticationManaging
private let featureDisabler: DataBrokerProtectionFeatureDisabling
private let pixelHandler: EventMapping<DataBrokerProtectionPixels>

init(subscriptionManager: SubscriptionManaging,
authRepository: AuthenticationRepository = KeychainAuthenticationData(),
featureDisabler: DataBrokerProtectionFeatureDisabling = DataBrokerProtectionFeatureDisabler()) {
self.subscriptionManager = subscriptionManager
self.authRepository = authRepository
init(featureDisabler: DataBrokerProtectionFeatureDisabling,
authenticationManager: DataBrokerProtectionAuthenticationManaging,
pixelHandler: EventMapping<DataBrokerProtectionPixels>) {
self.featureDisabler = featureDisabler
self.authenticationManager = authenticationManager
self.pixelHandler = pixelHandler
}

func registerForSubscriptionAccountManagerEvents() {
NotificationCenter.default.addObserver(self, selector: #selector(handleAccountDidSignOut), name: .accountDidSignOut, object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(handleAccountDidSignOut),
name: .accountDidSignOut,
object: nil)

NotificationCenter.default.addObserver(self,
selector: #selector(entitlementsDidChange),
name: .entitlementsDidChange,
object: nil)
}

@objc private func handleAccountDidSignOut() {
featureDisabler.disableAndDelete()
}

@objc private func entitlementsDidChange() {
Task { @MainActor in
do {
if try await authenticationManager.hasValidEntitlement() {
pixelHandler.fire(.entitlementCheckValid)
} else {
pixelHandler.fire(.entitlementCheckInvalid)
featureDisabler.disableAndDelete()
}
} catch {
/// We don't want to disable the agent in case of an error while checking for entitlements.
/// Since this is a destructive action, the only situation that should cause the data to be deleted and the agent to be removed is .success(false)
pixelHandler.fire(.entitlementCheckError)
assertionFailure("Error validating entitlement \(error)")
}
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// DataBrokerProtectionEntitlementMonitoring.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

protocol DataBrokerProtectionEntitlementMonitoring {
func start(checkEntitlementFunction: @escaping () async throws -> Bool, interval: TimeInterval, callback: @escaping (DataBrokerProtectionEntitlementMonitorResult) -> Void)
func stop()
}

public enum DataBrokerProtectionEntitlementMonitorResult {
case enabled
case disabled
case error
}

final class DataBrokerProtectionEntitlementMonitor: DataBrokerProtectionEntitlementMonitoring {
private var timer: Timer?

func start(checkEntitlementFunction: @escaping () async throws -> Bool, interval: TimeInterval, callback: @escaping (DataBrokerProtectionEntitlementMonitorResult) -> Void) {
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
Task {
do {
switch try await checkEntitlementFunction() {
case true:
callback(.enabled)
case false:
callback(.disabled)
}
} catch {
callback(.error)
}
}
}
}

func stop() {
timer?.invalidate()
timer = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ public enum DataBrokerProtectionPixels {
case initialScanSiteLoadDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double)
case initialScanPostLoadingDuration(duration: Double, hasError: Bool, brokerURL: String, sleepDuration: Double)
case initialScanPreStartDuration(duration: Double)

// Entitlements
case entitlementCheckValid
case entitlementCheckInvalid
case entitlementCheckError
}

extension DataBrokerProtectionPixels: PixelKitEvent {
Expand Down Expand Up @@ -267,6 +272,11 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
case .initialScanSiteLoadDuration: return "m_mac_dbp_scan_broker_site_loaded"
case .initialScanPostLoadingDuration: return "m_mac_dbp_initial_scan_broker_post_loading"
case .initialScanPreStartDuration: return "m_mac_dbp_initial_scan_pre_start_duration"

// Entitlements
case .entitlementCheckValid: return "m_mac_dbp_macos_entitlement_valid"
case .entitlementCheckInvalid: return "m_mac_dbp_macos_entitlement_invalid"
case .entitlementCheckError: return "m_mac_dbp_macos_entitlement_error"
}
}

Expand Down Expand Up @@ -361,7 +371,9 @@ extension DataBrokerProtectionPixels: PixelKitEvent {
.homeViewShowBadPathError,
.homeViewCTAMoveApplicationClicked,
.homeViewCTAGrantPermissionClicked,

.entitlementCheckValid,
.entitlementCheckInvalid,
.entitlementCheckError,
.secureVaultInitError,
.secureVaultError:
return [:]
Expand Down Expand Up @@ -486,7 +498,10 @@ public class DataBrokerProtectionPixelsHandler: EventMapping<DataBrokerProtectio
.homeViewShowWebUI,
.homeViewShowBadPathError,
.homeViewCTAMoveApplicationClicked,
.homeViewCTAGrantPermissionClicked:
.homeViewCTAGrantPermissionClicked,
.entitlementCheckValid,
.entitlementCheckInvalid,
.entitlementCheckError:
PixelKit.fire(event, frequency: .dailyAndCount)

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ public class DataBrokerProtectionAgentManagerProvider {
contentScopeProperties: contentScopeProperties,
emailService: emailService,
captchaService: captchaService)
let operationDependencies = DefaultDataBrokerOperationDependencies(

let agentstopper = DefaultDataBrokerProtectionAgentStopper(dataManager: dataManager,
entitlementMonitor: DataBrokerProtectionEntitlementMonitor(),
authenticationManager: authenticationManager,
pixelHandler: pixelHandler)

let operationDependencies = DefaultDataBrokerOperationDependencies(
database: dataManager.database,
config: executionConfig,
runnerProvider: runnerProvider,
Expand All @@ -86,7 +92,8 @@ public class DataBrokerProtectionAgentManagerProvider {
queueManager: queueManager,
dataManager: dataManager,
operationDependencies: operationDependencies,
pixelHandler: pixelHandler)
pixelHandler: pixelHandler,
agentStopper: agentstopper)
}
}

Expand All @@ -99,6 +106,7 @@ public final class DataBrokerProtectionAgentManager {
private let dataManager: DataBrokerProtectionDataManaging
private let operationDependencies: DataBrokerOperationDependencies
private let pixelHandler: EventMapping<DataBrokerProtectionPixels>
private let agentStopper: DataBrokerProtectionAgentStopper

// Used for debug functions only, so not injected
private lazy var browserWindowManager = BrowserWindowManager()
Expand All @@ -111,14 +119,17 @@ public final class DataBrokerProtectionAgentManager {
queueManager: DataBrokerProtectionQueueManager,
dataManager: DataBrokerProtectionDataManaging,
operationDependencies: DataBrokerOperationDependencies,
pixelHandler: EventMapping<DataBrokerProtectionPixels>) {
pixelHandler: EventMapping<DataBrokerProtectionPixels>,
agentStopper: DataBrokerProtectionAgentStopper
) {
self.userNotificationService = userNotificationService
self.activityScheduler = activityScheduler
self.ipcServer = ipcServer
self.queueManager = queueManager
self.dataManager = dataManager
self.operationDependencies = operationDependencies
self.pixelHandler = pixelHandler
self.agentStopper = agentStopper

self.activityScheduler.delegate = self
self.ipcServer.serverDelegate = self
Expand All @@ -127,20 +138,20 @@ public final class DataBrokerProtectionAgentManager {

public func agentFinishedLaunching() {

do {
// If there's no saved profile we don't need to start the scheduler
// Theoretically this should never happen, if there's no data, the agent shouldn't be running
guard (try dataManager.fetchProfile()) != nil else {
return
}
} catch {
os_log("Error during AgentManager.agentFinishedLaunching when trying to fetchProfile, error: %{public}@", log: .dataBrokerProtection, error.localizedDescription)
return
Task { @MainActor in
// The browser shouldn't start the agent if these prerequisites aren't met.
// However, since the agent can auto-start after a reboot without the browser, we need to validate it again.
// If the agent needs to be stopped, this function will stop it, so the subsequent calls after it will not be made.
await agentStopper.validateRunPrerequisitesAndStopAgentIfNecessary()

activityScheduler.startScheduler()
didStartActivityScheduler = true
queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil)

/// Monitors entitlement changes every 60 minutes to optimize system performance and resource utilization by avoiding unnecessary operations when entitlement is invalid.
/// While keeping the agent active with invalid entitlement has no significant risk, setting the monitoring interval at 60 minutes is a good balance to minimize backend checks.
agentStopper.monitorEntitlementAndStopAgentIfEntitlementIsInvalid(interval: .minutes(60))
}

activityScheduler.startScheduler()
didStartActivityScheduler = true
queueManager.startScheduledOperationsIfPermitted(showWebView: false, operationDependencies: operationDependencies, completion: nil)
}
}

Expand Down
Loading
Loading