1 import Base 2 import bend-kit-files@0.1.0.0/files.bend as Fs 3 import ../../../effs/io.bend as X 4 import ../../../plan/type.bend as P 5 import ./type.bend as St 6 import ./ops.bend as StO 7 8 # The Mac's settings as they are, read with read-only programs (defaults export, pmset -g, scutil --get, 9 # socketfilterfw --get*, plutil -extract, networksetup -get*, hidutil --get, nvram, readlink, stat, dscl, 10 # shasum), and what applying the declared ones would change. Nothing is written. 11 12 # ---- system.defaults: each domain's plist exported once, its options read from the XML ---- 13 def read_domains(ds: List<&2, String>, +xs: List<&2, St.Setting>) -> IO(List<&2, St.Setting>): 14 match ds: 15 case []: 16 IO.pure(List<&2, St.Setting>, []) 17 case +d <> rest: 18 do IO>: 19 xml : String <- X.run("defaults", StO.export_args(d)) 20 more : List<&2, St.Setting> <- read_domains(rest, xs) 21 return List.append(&2, St.Setting, StO.read_domain(xs, d, StO.text_lines(xml)), more) 22 23 # The observed values of the options (their values are ignored) that live in a defaults domain. 24 def defaults_now(+xs: List<&2, St.Setting>) -> IO(List<&2, St.Setting>): 25 read_domains(StO.domains(xs), xs) 26 27 # ---- power and names ---- 28 def power() -> IO(List<&2, St.Setting>): 29 do IO>: 30 pm : String <- X.run("pmset", ["-g", "custom"]) 31 return StO.power_settings(pm) 32 33 # power.restartAfterFreeze is left out: only `systemsetup` reports it, and only to an administrator. 34 def power_button() -> IO(List<&2, St.Setting>): 35 do IO>: 36 pm : String <- X.run("pmset", ["-g"]) 37 return StO.button_setting(pm) 38 39 def name(+option: String, +key: String) -> IO(List<&2, St.Setting>): 40 do IO>: 41 +v : String <- X.run("scutil", ["--get", key]) 42 return StO.when_filled(v, StO.str_setting(option, String.trim(v))) 43 44 def names() -> IO(List<&2, St.Setting>): 45 do IO>: 46 a : List<&2, St.Setting> <- name("networking.computerName", "ComputerName") 47 b : List<&2, St.Setting> <- name("networking.localHostName", "LocalHostName") 48 c : List<&2, St.Setting> <- name("networking.hostName", "HostName") 49 return List.concat(&2, St.Setting, [a, b, c]) 50 51 # ---- the firewall ---- 52 def socketfilterfw(+flag: String) -> IO(String): 53 X.run("/usr/libexec/ApplicationFirewall/socketfilterfw", [flag]) 54 55 def firewall_now() -> IO(St.Firewall): 56 do IO: 57 a : String <- socketfilterfw("--getglobalstate") 58 b : String <- socketfilterfw("--getallowsigned") 59 c : String <- socketfilterfw("--getblockall") 60 d : String <- socketfilterfw("--getstealthmode") 61 return StO.firewall_of(a ++ b ++ c ++ d) 62 63 # ---- Dock tiles ---- 64 def field(+plist: String, +tile: String, +f: String) -> IO(String): 65 do IO: 66 out : String <- X.run("plutil", ["-extract", tile ++ "." ++ f, "raw", plist]) 67 return String.trim(out) 68 69 def tiles(n: Nat, +plist: String, +key: String, +i: Nat) -> IO(List<&2, String>): 70 match n: 71 case 0n: 72 IO.pure(List<&2, String>, []) 73 case 1n+m: 74 do IO>: 75 +tile : String = key ++ "." ++ Nat.show(i) 76 t : String <- field(plist, tile, "tile-type") 77 u : String <- field(plist, tile, "tile-data.file-data._CFURLString") 78 a : String <- field(plist, tile, "tile-data.arrangement") 79 d : String <- field(plist, tile, "tile-data.displayas") 80 s : String <- field(plist, tile, "tile-data.showas") 81 rest : List<&2, String> <- tiles(m, plist, key, 1n+i) 82 return StO.tile_nix(key, t, u, a, d, s) <> rest 83 84 # `plutil -extract raw` answers the array's length. 85 def dock_list(+plist: String, +key: String) -> IO(List<&2, St.Setting>): 86 do IO>: 87 +n : String <- X.run("plutil", ["-extract", key, "raw", plist]) 88 ts : List<&2, String> <- tiles(StO.nat_of(n), plist, key, 0n) 89 return StO.dock_setting(key, n, ts) 90 91 # ---- network ---- 92 def per_service(ss: List<&2, String>, +get: String) -> IO(List<&2, String>): 93 match ss: 94 case []: 95 IO.pure(List<&2, String>, []) 96 case s <> rest: 97 do IO>: 98 out : String <- X.run("networksetup", [get, s]) 99 more : List<&2, String> <- per_service(rest, get) 100 return StO.per_service(out) <> more 101 102 def network() -> IO(List<&2, St.Setting>): 103 do IO>: 104 out : String <- X.run("networksetup", ["-listallnetworkservices"]) 105 +ss : List<&2, String> = StO.services(out) 106 dns : List<&2, String> <- per_service(ss, "-getdnsservers") 107 search : List<&2, String> <- per_service(ss, "-getsearchdomains") 108 return [StO.nix_setting("networking.knownNetworkServices", StO.nix_list(ss)), 109 StO.nix_setting("networking.dns", StO.same_for_all("DNS servers", dns)), 110 StO.nix_setting("networking.search", StO.same_for_all("search domains", search))] 111 112 # ---- security ---- 113 # Whether nix-darwin manages a file: it links it to /etc/static. 114 def nix_link(+path: String) -> IO(Bool): 115 do IO: 116 t : String <- X.run("readlink", [path]) 117 return StO.is_nix_link(t) 118 119 def security() -> IO(List<&2, St.Setting>): 120 do IO>: 121 +pam : String <- X.read("/etc/pam.d/sudo_local") 122 pam_nix : Bool <- nix_link("/etc/pam.d/sudo_local") 123 +sudo : String <- X.read("/etc/sudoers.d/10-nix-darwin-extra-config") 124 pki : Bool <- nix_link("/etc/ssl/certs/ca-certificates.crt") 125 return [StO.bool_setting("security.pam.services.sudo_local.enable", pam_nix), 126 StO.bool_setting("security.pam.services.sudo_local.touchIdAuth", String.contains(pam, "pam_tid.so")), 127 StO.bool_setting("security.pam.services.sudo_local.watchIdAuth", String.contains(pam, "pam_watchid")), 128 StO.bool_setting("security.pam.services.sudo_local.reattach", String.contains(pam, "pam_reattach")), 129 StO.bool_setting("security.sudo.keepTerminfo", String.contains(sudo, "TERMINFO_DIRS")), 130 StO.str_setting("security.sudo.extraConfig", StO.sudo_extra(sudo)), 131 StO.bool_setting("security.pki.installCACerts", pki)] 132 133 def keyboard() -> IO(List<&2, St.Setting>): 134 do IO>: 135 km : String <- X.run("hidutil", ["property", "--get", "UserKeyMapping"]) 136 return StO.keyboard_settings(km) 137 138 # ---- startup chime, time zone, primary user, Nix, shells, fonts, SSH ---- 139 def names_of(r: Result<&1, &1, U32 & String, List<&2, String>>) -> List<&2, String>: 140 match r: 141 case Done{ns}: 142 ns 143 case Fail{_}: 144 [] 145 146 def system() -> IO(List<&2, St.Setting>): 147 do IO>: 148 +mute : String <- X.run("nvram", ["StartupMute"]) 149 tz : String <- X.run("readlink", ["/etc/localtime"]) 150 user : String <- X.run("stat", ["-f", "%Su", "/dev/console"]) 151 nix : Bool <- nix_link("/etc/nix/nix.conf") 152 shells : String <- X.read("/etc/shells") 153 fonts : Result<&1, &1, U32 & String, List<&2, String>> <- Fs.list_dir("/Library/Fonts/Nix Fonts") 154 zsh : Bool <- nix_link("/etc/zshrc") 155 bash : Bool <- nix_link("/etc/bashrc") 156 fish : Bool <- nix_link("/etc/fish/config.fish") 157 ssh : Bool <- X.succeeds("launchctl", ["print", "system/com.openssh.sshd"]) 158 return List.append(&2, St.Setting, StO.chime_setting(mute), 159 [StO.str_setting("time.timeZone", StO.time_zone(tz)), 160 StO.str_setting("system.primaryUser", String.trim(user)), 161 StO.bool_setting("nix.enable", nix), 162 StO.nix_setting("environment.shells", StO.nix_list(StO.extra_shells(shells))), 163 StO.fonts_setting(names_of(fonts)), 164 StO.bool_setting("programs.zsh.enable", zsh), 165 StO.bool_setting("programs.bash.enable", bash), 166 StO.bool_setting("programs.fish.enable", fish), 167 StO.bool_setting("services.openssh.enable", ssh)]) 168 169 # ---- users and groups ---- 170 def dscl(+record: String, +attr: String) -> IO(String): 171 do IO: 172 out : String <- X.run("dscl", [".", "-read", record, attr]) 173 return StO.dscl_value(attr, out) 174 175 def user_list(ns: List<&2, String>) -> IO(List<&2, St.Setting>): 176 match ns: 177 case []: 178 IO.pure(List<&2, St.Setting>, []) 179 case +n <> rest: 180 do IO>: 181 uid : String <- dscl("/Users/" ++ n, "UniqueID") 182 gid : String <- dscl("/Users/" ++ n, "PrimaryGroupID") 183 dir : String <- dscl("/Users/" ++ n, "NFSHomeDirectory") 184 real : String <- dscl("/Users/" ++ n, "RealName") 185 shell : String <- dscl("/Users/" ++ n, "UserShell") 186 hidden : String <- dscl("/Users/" ++ n, "IsHidden") 187 more : List<&2, St.Setting> <- user_list(rest) 188 return List.append(&2, St.Setting, StO.user_settings(n, uid, gid, dir, real, shell, hidden), more) 189 190 def group_list(ns: List<&2, String>) -> IO(List<&2, St.Setting>): 191 match ns: 192 case []: 193 IO.pure(List<&2, St.Setting>, []) 194 case +n <> rest: 195 do IO>: 196 gid : String <- dscl("/Groups/" ++ n, "PrimaryGroupID") 197 real : String <- dscl("/Groups/" ++ n, "RealName") 198 members : String <- dscl("/Groups/" ++ n, "GroupMembership") 199 more : List<&2, St.Setting> <- group_list(rest) 200 return List.append(&2, St.Setting, StO.group_settings(n, gid, real, members), more) 201 202 def users() -> IO(List<&2, St.Setting>): 203 do IO>: 204 us : String <- X.run("dscl", [".", "-list", "/Users", "UniqueID"]) 205 gs : String <- X.run("dscl", [".", "-list", "/Groups", "PrimaryGroupID"]) 206 a : List<&2, St.Setting> <- user_list(StO.accounts(us)) 207 b : List<&2, St.Setting> <- group_list(StO.accounts(gs)) 208 return List.concat(&2, St.Setting, [[StO.nix_setting("users.knownUsers", "[ ]"), StO.nix_setting("users.knownGroups", "[ ]")], a, b]) 209 210 # ---- launchd files, read only: each is named by its content hash, but not copied into the system's 211 # nix/launch/ (the old import did), so a declaration that uses one must copy it there itself ---- 212 def regular(k: Fs.FileType) -> Bool: 213 match k: 214 case Fs.Regular{}: 215 True{} 216 case _: 217 False{} 218 219 def is_file(r: Result<&1, &1, U32 & String, Fs.Info>) -> Bool: 220 match r: 221 case Done{i}: 222 match i: 223 case Fs.Info{k, _, _}: 224 regular(k) 225 case Fail{_}: 226 False{} 227 228 def launch_files(fs: List<&2, String>, +opt: String, +dir: String, +labels: List<&2, String>) -> IO(List<&2, St.Setting>): 229 match fs: 230 case []: 231 IO.pure(List<&2, St.Setting>, []) 232 case +f <> rest: 233 do IO>: 234 info : Result<&1, &1, U32 & String, Fs.Info> <- Fs.stat(dir ++ "/" ++ f) 235 +keep : Bool = is_file(info) && StO.launch_kept(f, labels) 236 sum : String <- Bool.pick(IO(String), keep, X.run("shasum", ["-a", "256", dir ++ "/" ++ f]), IO.pure(String, "")) 237 more : List<&2, St.Setting> <- launch_files(rest, opt, dir, labels) 238 return List.append(&2, St.Setting, Bool.pick(List<&2, St.Setting>, keep, [StO.launch_setting(opt, f, sum)], []), more) 239 240 def launch_dir(+opt: String, +dir: String, +labels: List<&2, String>) -> IO(List<&2, St.Setting>): 241 do IO>: 242 r : Result<&1, &1, U32 & String, List<&2, String>> <- Fs.list_dir(dir) 243 launch_files(names_of(r), opt, dir, labels) 244 245 # `labels`: the system's own launchd jobs (nix-darwin writes them; they are not the Mac's settings). 246 def launch(+labels: List<&2, String>) -> IO(List<&2, St.Setting>): 247 do IO>: 248 h : String <- X.home() 249 a : List<&2, St.Setting> <- launch_dir("environment.launchDaemons", "/Library/LaunchDaemons", labels) 250 b : List<&2, St.Setting> <- launch_dir("environment.launchAgents", "/Library/LaunchAgents", labels) 251 c : List<&2, St.Setting> <- launch_dir("environment.userLaunchAgents", h ++ "/Library/LaunchAgents", labels) 252 return List.concat(&2, St.Setting, [a, b, c]) 253 254 # ---- all of it ---- 255 # The Mac's values for the options `xs` (their values are ignored) and everything nix-darwin sets outside 256 # system.defaults. 257 def observed_from(+xs: List<&2, St.Setting>, +labels: List<&2, String>) -> IO(List<&2, St.Setting>): 258 do IO>: 259 prefs : List<&2, St.Setting> <- defaults_now(xs) 260 pw : List<&2, St.Setting> <- power() 261 nm : List<&2, St.Setting> <- names() 262 +h : String <- X.home() 263 apps : List<&2, St.Setting> <- dock_list(h ++ "/Library/Preferences/com.apple.dock.plist", "persistent-apps") 264 others : List<&2, St.Setting> <- dock_list(h ++ "/Library/Preferences/com.apple.dock.plist", "persistent-others") 265 net : List<&2, St.Setting> <- network() 266 btn : List<&2, St.Setting> <- power_button() 267 sec : List<&2, St.Setting> <- security() 268 kb : List<&2, St.Setting> <- keyboard() 269 sys : List<&2, St.Setting> <- system() 270 us : List<&2, St.Setting> <- users() 271 ld : List<&2, St.Setting> <- launch(labels) 272 return List.concat(&2, St.Setting, [prefs, pw, nm, apps, others, net, btn, sec, kb, sys, us, ld]) 273 274 # The Mac's settings: every nix-darwin option in `options_tsv` (nix-darwin's option list, "optiontype") 275 # that it has a value for, and the rest, less the system's own launchd jobs `labels`. 276 def observed(+options_tsv: String, +labels: List<&2, String>) -> IO(List<&2, St.Setting>): 277 do IO>: 278 tsv : String <- X.read(options_tsv) 279 observed_from(StO.options(tsv), labels) 280 281 # What applying the declared settings and firewall would change: each declared setting whose observed 282 # value differs ("(unset)" when the Mac has none), and the firewall. 283 def changes(+settings: List<&2, St.Setting>, fw: St.Firewall) -> IO(List<&2, P.Step>): 284 do IO>: 285 now : List<&2, St.Setting> <- observed_from(settings, []) 286 f : St.Firewall <- firewall_now() 287 return List.append(&2, P.Step, StO.firewall_change(fw, f), StO.diff(settings, now))