Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 8 additions & 8 deletions Sources/app/ViewModels/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// BootstrapMate
//
// Manages all configuration settings for the GUI.
// Reads current values from ConfigManager and MDM status from MDMDetector.
// Reads current values from ConfigManager and management status from ManagementDetector.
// Writes changes via XPC helper for system-level persistence.
//

Expand Down Expand Up @@ -127,11 +127,11 @@ final class SettingsViewModel {
manifestPreviewState = .idle
}

// MARK: - MDM Status
// MARK: - Management

private(set) var managedKeys: Set<String> = []

func isMDMManaged(_ key: String) -> Bool {
func isManaged(_ key: String) -> Bool {
managedKeys.contains(key)
}

Expand All @@ -143,7 +143,7 @@ final class SettingsViewModel {

ConfigManager.shared.reloadPreferences()
let config = ConfigManager.shared.config
let detector = MDMDetector.shared
let detector = ManagementDetector.shared

managedKeys = detector.allManagedKeys()

Expand All @@ -169,10 +169,10 @@ final class SettingsViewModel {

// MARK: - Saving

/// Saves all non-MDM-managed settings via the XPC helper.
/// Saves all non-managed settings via the XPC helper.
func save(using client: XPCClient) {
func saveString(_ key: String, _ value: String) {
guard !isMDMManaged(key) else { return }
guard !isManaged(key) else { return }
if value.isEmpty {
client.removePreference(key: key)
} else {
Expand All @@ -181,12 +181,12 @@ final class SettingsViewModel {
}

func saveBool(_ key: String, _ value: Bool) {
guard !isMDMManaged(key) else { return }
guard !isManaged(key) else { return }
client.setBoolPreference(key: key, value: value)
}

func saveInt(_ key: String, _ value: Int) {
guard !isMDMManaged(key) else { return }
guard !isManaged(key) else { return }
client.setIntPreference(key: key, value: value)
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/app/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// BootstrapMate
//
// Main tab with centered app info header and settings below.
// MDM-managed settings display a lock icon and are disabled.
// Managed settings display a lock icon and are disabled.
//

import SwiftUI
Expand Down Expand Up @@ -308,7 +308,7 @@ struct SettingsView: View {
label: String? = nil,
@ViewBuilder content: () -> Content
) -> some View {
let managed = viewModel.isMDMManaged(key)
let managed = viewModel.isManaged(key)
VStack(alignment: .leading, spacing: 2) {
if let label {
Text(label)
Expand All @@ -318,7 +318,7 @@ struct SettingsView: View {
content()
.disabled(managed)
if managed {
Label("Managed by MDM", systemImage: "lock.fill")
Label("Managed", systemImage: "lock.fill")
.font(.caption)
.foregroundStyle(.secondary)
}
Expand Down
38 changes: 19 additions & 19 deletions Sources/cli/bootstrapmate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// BootstrapMate
//
// CLI entry point for BootstrapMate - Swift deployment utility for ADE-driven macOS setup.
// Supports MDM managed preferences, CLI arguments, and graceful fallbacks.
// Supports managed preferences, CLI arguments, and graceful fallbacks.
//

import Foundation
Expand Down Expand Up @@ -192,39 +192,39 @@ struct BootstrapMate: ParsableCommand {
}
}

// Wait for MDM configuration profile to be applied (during Setup Assistant)
// The MDM profile with url preference may not be applied immediately at boot
let mdmTimeout = 300 // 5 minutes
// Wait for management configuration profile to be applied (during Setup Assistant)
// The management profile with url preference may not be applied immediately at boot
let managementTimeout = 300 // 5 minutes
if jsonurl == nil || jsonurl!.isEmpty {
// No CLI URL provided, we need MDM config
// No CLI URL provided, we need management config
if !ConfigManager.shared.isValid() {
Logger.info("Waiting for MDM configuration profile (timeout: \(mdmTimeout)s)...")
earlyLog("Waiting for MDM config...")
Logger.info("Waiting for management configuration profile (timeout: \(managementTimeout)s)...")
earlyLog("Waiting for management config...")
Comment on lines +201 to +202

for i in 1...mdmTimeout {
// Reload preferences from MDM domains
for i in 1...managementTimeout {
// Reload preferences from management domains
if ConfigManager.shared.reloadManagedPreferences() {
Logger.success("MDM configuration received after \(i)s")
earlyLog("MDM config received after \(i)s")
Logger.success("Management configuration received after \(i)s")
earlyLog("Management config received after \(i)s")
break
}

// Log progress every 30 seconds
if i % 30 == 0 {
Logger.debug("Still waiting for MDM config... (\(i)s elapsed)")
Logger.debug("Still waiting for management config... (\(i)s elapsed)")
}

Thread.sleep(forTimeInterval: 1)

if i == mdmTimeout {
Logger.warning("MDM configuration not received within \(mdmTimeout)s")
earlyLog("MDM config timeout after \(mdmTimeout)s")
if i == managementTimeout {
Logger.warning("Management configuration not received within \(managementTimeout)s")
earlyLog("Management config timeout after \(managementTimeout)s")
}
}
}
}

// Apply CLI arguments to ConfigManager (overrides MDM settings)
// Apply CLI arguments to ConfigManager (overrides management settings)
ConfigManager.shared.applyCliArguments(
jsonUrl: jsonurl,
headers: headers,
Expand Down Expand Up @@ -268,15 +268,15 @@ struct BootstrapMate: ParsableCommand {
}
} else {
// No URL provided - check if we have embedded config or should fail
Logger.warning("No JSON URL provided via CLI or MDM")
Logger.warning("No JSON URL provided via CLI or managed preferences")

// Try to fetch from ConfigManager's external config
if ConfigManager.shared.fetchExternalConfig() {
Logger.info("Loaded external config from MDM preferences")
Logger.info("Loaded external config from management preferences")
// Convert BootstrapConfig to manifest loading
// For now, we require jsonUrl
} else {
Logger.error("No manifest URL configured. Use --jsonurl or configure via MDM profile.")
Logger.error("No manifest URL configured. Use --jsonurl or configure via management profile.")
Foundation.exit(1)
}
}
Expand Down
28 changes: 14 additions & 14 deletions Sources/core/Managers/ConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//
// Configuration loader with fallback chain:
// 1. CLI arguments (highest priority)
// 2. MDM managed preferences
// 2. managed preferences
// 3. Embedded/default values (lowest priority)
//

Expand Down Expand Up @@ -83,9 +83,9 @@ public struct BootstrapMateConfig {
public final class ConfigManager {
nonisolated(unsafe) public static let shared = ConfigManager()

// MDM preference domains to check (in order of priority)
private let mdmPreferenceDomains = [
"com.github.bootstrapmate", // Primary BootstrapMate domain (MDM profile)
// management preference domains to check (in order of priority)
private let managementPreferenceDomains = [
"com.github.bootstrapmate", // Primary BootstrapMate domain (management profile)
"io.macadmins.installapplications" // InstallApplications compatibility
]

Expand All @@ -102,13 +102,13 @@ public final class ConfigManager {
// Start with defaults
self.config = BootstrapMateConfig()

// Load MDM managed preferences as baseline
// Load managed preferences as baseline
loadManagedPreferences()
}

// MARK: - Public API

/// Apply CLI arguments (highest priority - overrides MDM settings)
/// Apply CLI arguments (highest priority - overrides management settings)
public func applyCliArguments(
jsonUrl: String? = nil,
headers: String? = nil,
Expand Down Expand Up @@ -194,14 +194,14 @@ public final class ConfigManager {
return config.jsonUrl != nil && !config.jsonUrl!.isEmpty
}

/// Reload MDM managed preferences (call when waiting for MDM profile to be applied)
/// Reload managed preferences (call when waiting for management profile to be applied)
/// Returns true if a valid JSON URL was found
public func reloadManagedPreferences() -> Bool {
// Clear existing URL to force re-read
config.jsonUrl = nil

// Try each domain in priority order
for domain in mdmPreferenceDomains {
for domain in managementPreferenceDomains {
if loadPreferencesFromDomain(domain) {
if config.jsonUrl != nil && !config.jsonUrl!.isEmpty {
Logger.info("Loaded managed preferences from: \(domain)")
Expand All @@ -210,7 +210,7 @@ public final class ConfigManager {
}
}

// Also check for managed preferences via MDM profile
// Also check for managed preferences via management profile
loadFromManagedAppConfig()

return config.jsonUrl != nil && !config.jsonUrl!.isEmpty
Expand Down Expand Up @@ -276,21 +276,21 @@ public final class ConfigManager {
Logger.debug("Loading managed preferences...")

// Try each domain in priority order
for domain in mdmPreferenceDomains {
for domain in managementPreferenceDomains {
if loadPreferencesFromDomain(domain) {
Logger.info("Loaded managed preferences from: \(domain)")
return
}
}

// Also check for managed preferences via MDM profile
// Also check for managed preferences via management profile
loadFromManagedAppConfig()

Logger.debug("No MDM managed preferences found, using defaults")
Logger.debug("No managed preferences found, using defaults")
}

private func loadPreferencesFromDomain(_ domain: String) -> Bool {
// First try CFPreferences (works better for MDM-pushed preferences)
// First try CFPreferences (works better for management-pushed preferences)
let cfDomain = domain as CFString

// Check for URL key (various names)
Expand Down Expand Up @@ -432,7 +432,7 @@ public final class ConfigManager {
}

private func loadFromManagedAppConfig() {
// Check for MDM-deployed configuration profile
// Check for management-deployed configuration profile
// This handles the case where config is delivered via custom configuration profile
let configDomains = [
"com.github.bootstrapmate"
Expand Down
4 changes: 2 additions & 2 deletions Sources/core/Managers/ManifestManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public final class ManifestManager {
dryRun = enable
}

public func loadFromMDMOrLocal() {
Logger.log("Loading manifest from MDM or local fallback.")
public func loadFromManagementOrLocal() {
Logger.log("Loading manifest from management or local fallback.")
// TODO: Implement managed preferences fallback handling.
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/core/Managers/StatusManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// StatusManager.swift
// BootstrapMate
//
// Tracks installation status via plist and JSON for MDM detection.
// Tracks installation status via plist and JSON for management detection.
// macOS equivalent of Windows StatusManager.cs using property lists.
//

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
//
// MDMDetector.swift
// ManagementDetector.swift
// BootstrapMate
//
// Detects which preferences are managed by MDM configuration profiles
// Detects which preferences are managed by configuration profiles
// vs. set locally. Used by the GUI to show lock indicators.
//

import Foundation

public final class MDMDetector: Sendable {
public final class ManagementDetector: Sendable {

public static let shared = MDMDetector()
public static let shared = ManagementDetector()

/// Preference domains checked for MDM management, in priority order.
/// Preference domains checked for management, in priority order.
private let managedDomains = [
"com.github.bootstrapmate",
"io.macadmins.installapplications"
Expand All @@ -36,7 +36,7 @@ public final class MDMDetector: Sendable {
// MARK: - Public API

/// Returns true if the canonical key is present in any managed preferences plist.
public func isManagedByMDM(key: String) -> Bool {
public func isManaged(key: String) -> Bool {
let keysToCheck = Self.keyAliases[key] ?? [key]
for domain in managedDomains {
for plistPath in managedPlistPaths(for: domain) {
Expand All @@ -49,7 +49,7 @@ public final class MDMDetector: Sendable {
return false
}

/// Returns the MDM-managed value for a canonical key, or nil.
/// Returns the managed value for a canonical key, or nil.
public func managedValue(forKey key: String) -> Any? {
let keysToCheck = Self.keyAliases[key] ?? [key]
for domain in managedDomains {
Expand All @@ -63,7 +63,7 @@ public final class MDMDetector: Sendable {
return nil
}

/// Returns the set of canonical keys that are MDM-managed.
/// Returns the set of canonical keys that are managed.
public func allManagedKeys() -> Set<String> {
var result = Set<String>()
for (canonical, aliases) in Self.keyAliases {
Expand Down
Loading