domain/architecture/system/deployment/nix_darwin/setting/effects.bend

near-v-framework@0.5.0.0 · 287 lines · 0 definitions

1import Base
2import bend-kit-files@0.1.0.0/files.bend as Fs
3import near-v-framework@0.5.0.0/domain/architecture/system/effs/io.bend as X
4import near-v-framework@0.5.0.0/domain/architecture/system/plan/type.bend as P
5import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/setting/type.bend as St
6import near-v-framework@0.5.0.0/domain/architecture/system/deployment/nix_darwin/setting/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 ----
13def 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<List<&2, St.Setting>>:
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.
24def defaults_now(+xs: List<&2, St.Setting>) -> IO(List<&2, St.Setting>):
25  read_domains(StO.domains(xs), xs)
26
27# ---- power and names ----
28def power() -> IO(List<&2, St.Setting>):
29  do IO<List<&2, St.Setting>>:
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.
34def power_button() -> IO(List<&2, St.Setting>):
35  do IO<List<&2, St.Setting>>:
36    pm : String <- X.run("pmset", ["-g"])
37    return StO.button_setting(pm)
38
39def name(+option: String, +key: String) -> IO(List<&2, St.Setting>):
40  do IO<List<&2, St.Setting>>:
41    +v : String <- X.run("scutil", ["--get", key])
42    return StO.when_filled(v, StO.str_setting(option, String.trim(v)))
43
44def names() -> IO(List<&2, St.Setting>):
45  do IO<List<&2, St.Setting>>:
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 ----
52def socketfilterfw(+flag: String) -> IO(String):
53  X.run("/usr/libexec/ApplicationFirewall/socketfilterfw", [flag])
54
55def firewall_now() -> IO(St.Firewall):
56  do IO<St.Firewall>:
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 ----
64def field(+plist: String, +tile: String, +f: String) -> IO(String):
65  do IO<String>:
66    out : String <- X.run("plutil", ["-extract", tile ++ "." ++ f, "raw", plist])
67    return String.trim(out)
68
69def 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<List<&2, String>>:
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 <array> raw` answers the array's length.
85def dock_list(+plist: String, +key: String) -> IO(List<&2, St.Setting>):
86  do IO<List<&2, St.Setting>>:
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 ----
92def 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<List<&2, String>>:
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
102def network() -> IO(List<&2, St.Setting>):
103  do IO<List<&2, St.Setting>>:
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.
114def nix_link(+path: String) -> IO(Bool):
115  do IO<Bool>:
116    t : String <- X.run("readlink", [path])
117    return StO.is_nix_link(t)
118
119def security() -> IO(List<&2, St.Setting>):
120  do IO<List<&2, St.Setting>>:
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
133def keyboard() -> IO(List<&2, St.Setting>):
134  do IO<List<&2, St.Setting>>:
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 ----
139def 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
146def system() -> IO(List<&2, St.Setting>):
147  do IO<List<&2, St.Setting>>:
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 ----
170def dscl(+record: String, +attr: String) -> IO(String):
171  do IO<String>:
172    out : String <- X.run("dscl", [".", "-read", record, attr])
173    return StO.dscl_value(attr, out)
174
175def 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<List<&2, St.Setting>>:
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
190def 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<List<&2, St.Setting>>:
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
202def users() -> IO(List<&2, St.Setting>):
203  do IO<List<&2, St.Setting>>:
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 ----
212def regular(k: Fs.FileType) -> Bool:
213  match k:
214    case Fs.Regular{}:
215      True{}
216    case _:
217      False{}
218
219def 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
228def 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<List<&2, St.Setting>>:
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
240def launch_dir(+opt: String, +dir: String, +labels: List<&2, String>) -> IO(List<&2, St.Setting>):
241  do IO<List<&2, St.Setting>>:
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).
246def launch(+labels: List<&2, String>) -> IO(List<&2, St.Setting>):
247  do IO<List<&2, St.Setting>>:
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.
257def observed_from(+xs: List<&2, St.Setting>, +labels: List<&2, String>) -> IO(List<&2, St.Setting>):
258  do IO<List<&2, St.Setting>>:
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, "option<TAB>type")
275# that it has a value for, and the rest, less the system's own launchd jobs `labels`.
276def observed(+options_tsv: String, +labels: List<&2, String>) -> IO(List<&2, St.Setting>):
277  do IO<List<&2, St.Setting>>:
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.
283def changes(+settings: List<&2, St.Setting>, fw: St.Firewall) -> IO(List<&2, P.Step>):
284  do IO<List<&2, P.Step>>:
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))