From 70a615967d851cac2634516b492fcbeb96e93ee9 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Thu, 2 May 2024 21:13:50 -0400 Subject: [PATCH] Error on SUIS (#941) * Rename macos planner to macos/mod * Clean up some clippy notes about specifying truncate * Add types for parsing macOS system profiles * Error on SUIS * Dump all profiles * Clean up errors * fixup: emit multiple errors on suis blocks * Clean up the error after user input * Nit on phrasing in warn * tpot -> blocking_policy * slice * Add a description to the fail pilst * Move around deck chairs * Disable GHA cache * clean up the query * profile sample: fail -> block * less technical * Link to a det.sys page that talks more about internal disks (tbd) * Update src/planner/macos/profiles.rs Co-authored-by: Cole Helbling * Expand the errorr message again * Test unknown does not error --------- Co-authored-by: Cole Helbling --- .github/workflows/build-aarch64-darwin.yml | 4 +- .github/workflows/build-aarch64-linux.yml | 4 +- .github/workflows/build-i686-linux.yml | 4 +- .github/workflows/build-x86_64-darwin.yml | 2 + .github/workflows/build-x86_64-linux.yml | 4 +- .github/workflows/ci.yml | 2 + .github/workflows/update.yml | 6 +- src/action/base/create_or_insert_into_file.rs | 1 + src/action/base/create_or_merge_nix_config.rs | 1 + src/action/macos/create_fstab_entry.rs | 1 + src/planner/{macos.rs => macos/mod.rs} | 45 +++++ src/planner/macos/profile.sample.block.plist | 58 +++++++ .../macos/profile.sample.unknown.plist | 49 ++++++ src/planner/macos/profile_queries.rs | 137 +++++++++++++++ src/planner/macos/profiles.rs | 162 ++++++++++++++++++ 15 files changed, 474 insertions(+), 6 deletions(-) rename src/planner/{macos.rs => macos/mod.rs} (88%) create mode 100644 src/planner/macos/profile.sample.block.plist create mode 100644 src/planner/macos/profile.sample.unknown.plist create mode 100644 src/planner/macos/profile_queries.rs create mode 100644 src/planner/macos/profiles.rs diff --git a/.github/workflows/build-aarch64-darwin.yml b/.github/workflows/build-aarch64-darwin.yml index 43fb2a0d5..693ae7ccf 100644 --- a/.github/workflows/build-aarch64-darwin.yml +++ b/.github/workflows/build-aarch64-darwin.yml @@ -1,6 +1,6 @@ name: Build aarch64 Darwin -on: +on: workflow_call: inputs: cache-key: @@ -23,6 +23,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Build the installer run: | nix build .#packages.aarch64-darwin.nix-installer -L diff --git a/.github/workflows/build-aarch64-linux.yml b/.github/workflows/build-aarch64-linux.yml index 645d93541..0ea3fb19d 100644 --- a/.github/workflows/build-aarch64-linux.yml +++ b/.github/workflows/build-aarch64-linux.yml @@ -1,6 +1,6 @@ name: Build aarch64 Linux (static) -on: +on: workflow_call: inputs: cache-key: @@ -23,6 +23,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Build the installer run: | nix build .#packages.aarch64-linux.nix-installer-static -L diff --git a/.github/workflows/build-i686-linux.yml b/.github/workflows/build-i686-linux.yml index 5db6e4145..6a9877700 100644 --- a/.github/workflows/build-i686-linux.yml +++ b/.github/workflows/build-i686-linux.yml @@ -1,6 +1,6 @@ name: Build i686 Linux (static) -on: +on: workflow_call: inputs: cache-key: @@ -23,6 +23,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Build the installer run: | nix build .#packages.i686-linux.nix-installer-static -L diff --git a/.github/workflows/build-x86_64-darwin.yml b/.github/workflows/build-x86_64-darwin.yml index 2e225bdc7..e8b838ddd 100644 --- a/.github/workflows/build-x86_64-darwin.yml +++ b/.github/workflows/build-x86_64-darwin.yml @@ -23,6 +23,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Build the installer run: | nix build .#packages.x86_64-darwin.nix-installer -L diff --git a/.github/workflows/build-x86_64-linux.yml b/.github/workflows/build-x86_64-linux.yml index 333335e0a..3578a2071 100644 --- a/.github/workflows/build-x86_64-linux.yml +++ b/.github/workflows/build-x86_64-linux.yml @@ -1,6 +1,6 @@ name: Build x86_64 Linux (static) -on: +on: workflow_call: inputs: cache-key: @@ -23,6 +23,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Build the installer run: | nix build .#packages.x86_64-linux.nix-installer-static -L diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0be010d78..fa27826e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,8 @@ jobs: with: flakehub: true - uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Check rustfmt run: nix develop --command check-rustfmt - name: Check Clippy diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 4ab751cc2..027002efc 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -2,7 +2,7 @@ name: update-flake-lock on: workflow_dispatch: schedule: - - cron: '0 0 * * 0' + - cron: "0 0 * * 0" jobs: lockfile: @@ -17,8 +17,10 @@ jobs: uses: DeterminateSystems/nix-installer-action@main with: flakehub: true - - name: Enable magic Nix cache + - name: Enable Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@main + with: + use-gha-cache: false - name: Check flake uses: DeterminateSystems/flake-checker-action@main - name: Update flake.lock diff --git a/src/action/base/create_or_insert_into_file.rs b/src/action/base/create_or_insert_into_file.rs index cf0e23749..96162a60f 100644 --- a/src/action/base/create_or_insert_into_file.rs +++ b/src/action/base/create_or_insert_into_file.rs @@ -208,6 +208,7 @@ impl Action for CreateOrInsertIntoFile { } let mut temp_file = OpenOptions::new() .create(true) + .truncate(true) .write(true) // If the file is created, ensure that it has harmless // permissions regardless of whether the mode will be diff --git a/src/action/base/create_or_merge_nix_config.rs b/src/action/base/create_or_merge_nix_config.rs index ae0585e32..98866cbeb 100644 --- a/src/action/base/create_or_merge_nix_config.rs +++ b/src/action/base/create_or_merge_nix_config.rs @@ -258,6 +258,7 @@ impl Action for CreateOrMergeNixConfig { } let mut temp_file = OpenOptions::new() .create(true) + .truncate(true) .write(true) // If the file is created, ensure that it has harmless // permissions regardless of whether the mode will be diff --git a/src/action/macos/create_fstab_entry.rs b/src/action/macos/create_fstab_entry.rs index 8eac22179..606f327eb 100644 --- a/src/action/macos/create_fstab_entry.rs +++ b/src/action/macos/create_fstab_entry.rs @@ -136,6 +136,7 @@ impl Action for CreateFstabEntry { let mut fstab = tokio::fs::OpenOptions::new() .create(true) + .truncate(false) .write(true) .read(true) .open(fstab_path) diff --git a/src/planner/macos.rs b/src/planner/macos/mod.rs similarity index 88% rename from src/planner/macos.rs rename to src/planner/macos/mod.rs index bb287b732..4e4139860 100644 --- a/src/planner/macos.rs +++ b/src/planner/macos/mod.rs @@ -8,6 +8,9 @@ use which::which; use super::ShellProfileLocations; use crate::planner::HasExpectedErrors; +mod profile_queries; +mod profiles; + use crate::{ action::{ base::RemoveDirectory, @@ -306,6 +309,7 @@ impl Planner for Macos { } async fn pre_install_check(&self) -> Result<(), PlannerError> { + check_suis().await?; check_not_running_in_rosetta()?; if self.enterprise_edition { check_enterprise_edition_available().await?; @@ -364,6 +368,43 @@ fn check_not_running_in_rosetta() -> Result<(), PlannerError> { Ok(()) } +async fn check_suis() -> Result<(), PlannerError> { + let policies: profiles::Policies = match profiles::load().await { + Ok(pol) => pol, + Err(e) => { + tracing::warn!( + "Skipping SystemUIServer checks: failed to load profile data: {:?}", + e + ); + return Ok(()); + }, + }; + + let blocks: Vec<_> = profile_queries::blocks_internal_mounting(&policies) + .into_iter() + .map(|blocking_policy| blocking_policy.display()) + .collect(); + + let error: String = match &blocks[..] { + [] => { + return Ok(()); + }, + [block] => format!( + "The following macOS configuration profile includes a 'Restrictions - Media' policy, which interferes with the Nix Store volume:\n\n{}\n\nSee https://determinate.systems/solutions/macos-internal-disk-policy", + block + ), + blocks => { + format!( + "The following macOS configuration profiles include a 'Restrictions - Media' policy, which interferes with the Nix Store volume:\n\n{}\n\nSee https://determinate.systems/solutions/macos-internal-disk-policy", + blocks.join("\n\n") + ) + }, + }; + + Err(MacosError::BlockedBySystemUIServerPolicy(error)) + .map_err(|e| PlannerError::Custom(Box::new(e))) +} + async fn check_enterprise_edition_available() -> Result<(), PlannerError> { tokio::fs::metadata("/usr/local/bin/determinate-nix-ee") .await @@ -377,12 +418,16 @@ async fn check_enterprise_edition_available() -> Result<(), PlannerError> { pub enum MacosError { #[error("`nix-darwin` installation detected, it must be removed before uninstalling Nix. Please refer to https://github.com/LnL7/nix-darwin#uninstalling for instructions how to uninstall `nix-darwin`.")] UninstallNixDarwin, + + #[error("{0}")] + BlockedBySystemUIServerPolicy(String), } impl HasExpectedErrors for MacosError { fn expected<'a>(&'a self) -> Option> { match self { this @ MacosError::UninstallNixDarwin => Some(Box::new(this)), + this @ MacosError::BlockedBySystemUIServerPolicy(_) => Some(Box::new(this)), } } } diff --git a/src/planner/macos/profile.sample.block.plist b/src/planner/macos/profile.sample.block.plist new file mode 100644 index 000000000..97abe5a98 --- /dev/null +++ b/src/planner/macos/profile.sample.block.plist @@ -0,0 +1,58 @@ + + + + + + foo + + + + ProfileDescription + The description + ProfileDisplayName + Don't allow mounting internal devices + ProfileIdentifier + MyProfile.6F6670A3-65AC-4EA4-8665-91F8FCE289AB + ProfileInstallDate + 2024-04-22 14:12:42 +0000 + ProfileType + Configuration + ProfileUUID + 6F6670A3-65AC-4EA4-8665-91F8FCE289AB + ProfileVersion + 1 + + + ProfileItems + + + + PayloadType + com.apple.systemuiserver + + PayloadContent + + mount-controls + + harddisk-internal + + + deny + + + + + + + + + diff --git a/src/planner/macos/profile.sample.unknown.plist b/src/planner/macos/profile.sample.unknown.plist new file mode 100644 index 000000000..571f94ef2 --- /dev/null +++ b/src/planner/macos/profile.sample.unknown.plist @@ -0,0 +1,49 @@ + + + + + _computerlevel + + + ProfileDescription + + ProfileDisplayName + macOS Software Update Policy: Mandatory Minor Upgrades + ProfileIdentifier + com.example + ProfileInstallDate + 2024-04-22 00:00:00 +0000 + ProfileItems + + + PayloadContent + + AllowPreReleaseInstallation + + AutomaticCheckEnabled + + + PayloadIdentifier + abc123 + PayloadType + com.apple.SoftwareUpdate + PayloadUUID + def456 + PayloadVersion + 1 + + + ProfileRemovalDisallowed + true + ProfileType + Configuration + ProfileUUID + F7972F85-2A4D-4609-A4BB-02CB0C34A3F8 + ProfileVerificationState + verified + ProfileVersion + 1 + + + + diff --git a/src/planner/macos/profile_queries.rs b/src/planner/macos/profile_queries.rs new file mode 100644 index 000000000..36fa23803 --- /dev/null +++ b/src/planner/macos/profile_queries.rs @@ -0,0 +1,137 @@ +use crate::planner::macos::profiles::{ + HardDiskInternalOpts, MountControls, Policies, Profile, ProfileItem, SystemUIServer, Target, +}; + +struct TargetProfileItem<'a> { + target: &'a Target, + profile: &'a Profile, + item: &'a ProfileItem, +} + +pub struct TargetProfileHardDiskInternalOpts<'a> { + pub target: &'a Target, + pub profile: &'a Profile, + pub opts: &'a [HardDiskInternalOpts], +} + +impl TargetProfileHardDiskInternalOpts<'_> { + pub fn display(&self) -> String { + let owner = match self.target { + crate::planner::macos::profiles::Target::Computer => { + "A computer-wide profile".to_string() + }, + crate::planner::macos::profiles::Target::User(u) => format!("A profile owned by {u}"), + }; + + let desc = [ + ("Name", &self.profile.profile_display_name), + ( + "Version", + &self.profile.profile_version.map(|v| v.to_string()), + ), + ("Description", &self.profile.profile_description), + ("ID", &self.profile.profile_identifier), + ("UUID", &self.profile.profile_uuid), + ("Installation Date", &self.profile.profile_install_date), + ] + .into_iter() + .filter_map(|(k, v)| Some((k, (*v).as_ref()?))) + .map(|(key, val)| format!(" * {}: {}", key, val)) + .collect::>() + .join("\n"); + + format!("{owner}:\n{}\n", desc) + } +} + +fn flatten(policies: &Policies) -> impl Iterator { + policies + .iter() + .flat_map(|(target, profiles): (&Target, &Vec)| { + profiles.iter().map(move |profile| (target, profile)) + }) + .flat_map(|(target, profile): (&Target, &Profile)| { + profile + .profile_items + .iter() + .map(move |item| TargetProfileItem { + target, + profile, + item, + }) + }) +} + +pub fn blocks_internal_mounting(policies: &Policies) -> Vec { + flatten(policies) + .filter_map(move |target_profile_item| { + let ProfileItem::SystemUIServer(system_ui_server) = target_profile_item.item else { + return None; + }; + let SystemUIServer { + mount_controls: Some(mount_controls), + } = system_ui_server + else { + return None; + }; + + let MountControls { harddisk_internal } = mount_controls; + + return Some(TargetProfileHardDiskInternalOpts { + target: target_profile_item.target, + profile: target_profile_item.profile, + opts: &harddisk_internal, + }); + }) + .filter(|TargetProfileHardDiskInternalOpts { opts, .. }| { + opts.iter().any(|x| { + [ + HardDiskInternalOpts::ReadOnly, + HardDiskInternalOpts::Deny, + HardDiskInternalOpts::Eject, + ] + .contains(x) + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_error() { + let parsed: Policies = plist::from_reader(std::io::Cursor::new(include_str!( + "./profile.sample.block.plist" + ))) + .unwrap(); + + let blocks = blocks_internal_mounting(&parsed); + let err = &blocks[0]; + + assert_eq!( + r#"A profile owned by foo: + * Name: Don't allow mounting internal devices + * Version: 1 + * Description: The description + * ID: MyProfile.6F6670A3-65AC-4EA4-8665-91F8FCE289AB + * UUID: 6F6670A3-65AC-4EA4-8665-91F8FCE289AB + * Installation Date: 2024-04-22 14:12:42 +0000"# + .trim() + .to_string(), + err.display().trim() + ); + } + + #[test] + fn no_error() { + let parsed: Policies = plist::from_reader(std::io::Cursor::new(include_str!( + "./profile.sample.unknown.plist" + ))) + .unwrap(); + + let blocks = blocks_internal_mounting(&parsed); + assert!(blocks.is_empty()); + } +} diff --git a/src/planner/macos/profiles.rs b/src/planner/macos/profiles.rs new file mode 100644 index 000000000..6e7ad864b --- /dev/null +++ b/src/planner/macos/profiles.rs @@ -0,0 +1,162 @@ +use std::collections::HashMap; + +use crate::execute_command; + +#[derive(thiserror::Error, Debug)] +pub enum LoadError { + #[error("Profile plist parsing error: {0}")] + Parse(#[from] plist::Error), + + #[error("Profile discovery error: {0}")] + ProfileListing(#[from] crate::ActionErrorKind), +} + +pub async fn load() -> Result { + let buf = execute_command( + tokio::process::Command::new("/usr/bin/profiles") + // "prints all configuration profiles to console" + .arg("-P") + // "path to output XML plist file (for -P, -L, -C). Use 'stdout' to send information to the console." + // NOTE(grahamc): `stdout` doesn't output XML formatting, but `stdout-xml` does + .args(["-o", "stdout-xml"]) + .stdin(std::process::Stdio::null()), + ) + .await? + .stdout; + + Ok(plist::from_reader(std::io::Cursor::new(buf))?) +} + +pub type Policies = HashMap>; + +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] +pub enum Target { + #[serde(rename(deserialize = "_computerlevel"))] + Computer, + #[serde(untagged)] + User(String), +} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub struct Profile { + pub profile_description: Option, + pub profile_display_name: Option, + pub profile_identifier: Option, + pub profile_install_date: Option, + #[serde(rename = "ProfileUUID")] + pub profile_uuid: Option, + pub profile_version: Option, + + #[serde(default)] + pub profile_items: Vec, +} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(tag = "PayloadType", content = "PayloadContent")] +pub enum ProfileItem { + #[serde(rename = "com.apple.systemuiserver")] + SystemUIServer(SystemUIServer), + + #[serde(untagged)] + Unknown(UnknownProfileItem), +} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub struct UnknownProfileItem { + payload_type: String, + payload_content: plist::Value, +} + +impl std::cmp::Eq for UnknownProfileItem {} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct SystemUIServer { + pub mount_controls: Option, +} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct MountControls { + #[serde(default)] + pub harddisk_internal: Vec, +} + +#[derive(serde::Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum HardDiskInternalOpts { + Authenticate, + ReadOnly, + Deny, + Eject, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn try_parse_blocking_policy() { + let parsed: Policies = plist::from_reader(std::io::Cursor::new(include_str!( + "./profile.sample.block.plist" + ))) + .unwrap(); + assert_eq!( + Policies::from([( + Target::User("foo".into()), + vec![Profile { + profile_description: Some("The description".into()), + profile_display_name: Some("Don't allow mounting internal devices".into()), + profile_identifier: Some( + "MyProfile.6F6670A3-65AC-4EA4-8665-91F8FCE289AB".into() + ), + profile_install_date: Some("2024-04-22 14:12:42 +0000".into()), + profile_uuid: Some("6F6670A3-65AC-4EA4-8665-91F8FCE289AB".into()), + profile_version: Some(1), + profile_items: vec![ProfileItem::SystemUIServer(SystemUIServer { + mount_controls: Some(MountControls { + harddisk_internal: vec![HardDiskInternalOpts::Deny], + }) + })], + }] + )]), + parsed + ); + } + + #[test] + fn try_parse_unknown() { + let parsed: Policies = plist::from_reader(std::io::Cursor::new(include_str!( + "./profile.sample.unknown.plist" + ))) + .unwrap(); + + assert_eq!( + Policies::from([( + Target::Computer, + vec![Profile { + profile_description: Some("".into()), + profile_display_name: Some( + "macOS Software Update Policy: Mandatory Minor Upgrades".into() + ), + profile_identifier: Some("com.example".into()), + profile_install_date: Some("2024-04-22 00:00:00 +0000".into()), + profile_uuid: Some("F7972F85-2A4D-4609-A4BB-02CB0C34A3F8".into()), + profile_version: Some(1), + profile_items: vec![ProfileItem::Unknown(UnknownProfileItem { + payload_type: "com.apple.SoftwareUpdate".into(), + payload_content: plist::Value::Dictionary({ + let mut dict = plist::dictionary::Dictionary::new(); + dict.insert("AllowPreReleaseInstallation".into(), false.into()); + dict.insert("AutomaticCheckEnabled".into(), true.into()); + dict + }) + })], + }] + )]), + parsed + ); + } +}