mirror of
https://github.com/bitsycore/Allofit.git
synced 2026-10-05 12:27:26 +00:00
Security - Elevated copies: root only reads the original; the copy is written by the user (sudo -u tee), so root never chowns / chmods a user-controlled path. Staging folder forced to 0700. - Service logs moved from fixed /tmp names to /Library/Logs/Allofit (root-owned) and ~/Library/Logs/Allofit; Diagnostics reveals instead of opening the log. - Index files are owner-only (0600; the root daemon's belongs to the installing user), set on the temp file before an atomic rename. - Root install passes the plist inline (base64, plutil -lint) instead of a user-writable temp file; the binary comes from Bundle.main. - Cache loader caps and checks the declared payload size; each save uses its own temp file. - Release action pinned to a commit; non-system LC_RPATHs stripped. Correctness - Move to Trash removes the files from the index (the watcher ignores the app's own operations) and registers Undo (Put Back); trashed folders are matched with the original URLs; failures are shown. - The saved event id stays below pending subtree walks (GUI and service). - Roots / exclusions changes restart the watcher from the snapshot's id. - Case-only renames no longer leave a ghost entry. - Service mode is saved only after a successful install; a saved but missing service falls back to the in-process indexer. Performance - New folders are merged without the O(n) removal pass. - Size / date sorts use a compact key array (539 -> 47 ms for 630k). - Selection, preview and actions use the selected records directly. - Service saves at most every 15 s; reader reloads pause while hidden and are deferred instead of dropped; window close saves only when dirty. Usability - Results appear during the first index; empty-list explanations. - Down arrow moves to the results, Up on the first row back; history on Up / Option-Up / Option-Down. - Search syntax popover and Help menu; shortcuts shown in the context menu; confirmations for Clear Cache and Uninstall; privacy usage strings; Group Containers excluded by default; wording, VoiceOver labels, plural.
84 lines
3.5 KiB
Swift
84 lines
3.5 KiB
Swift
import Foundation
|
|
|
|
// ElevatedAccess provides on-demand sudo-backed access to files the GUI
|
|
// user can't read directly. Common case: the root LaunchDaemon indexed
|
|
// `/Users/<otheruser>/...` (it has Full Disk Access), the GUI runs as
|
|
// the current user, and trying to render an inline preview hits a
|
|
// permission denial. The user clicks "Authorize" in the preview pane,
|
|
// AdminShell prompts for the password once, sudo copies the file to
|
|
// the per-user staging directory and chowns it to the GUI user.
|
|
//
|
|
// The staged copy is owned by the GUI user, lives in
|
|
// ~/Library/Caches/Allofit/elevated/
|
|
// and is wiped at app launch and at app quit so privileged copies don't
|
|
// linger on disk across sessions.
|
|
enum ElevatedAccess {
|
|
|
|
// per-user staging directory; lives under Library/Caches so macOS
|
|
// itself may purge it under disk-pressure, and our own cleanup() at
|
|
// launch + terminate keeps it from accumulating
|
|
static var stagingDirectory: URL {
|
|
let vCaches = FileManager.default.urls(
|
|
for: .cachesDirectory,
|
|
in: .userDomainMask
|
|
).first!
|
|
return vCaches.appendingPathComponent("Allofit/elevated", isDirectory: true)
|
|
}
|
|
|
|
// true if the current user can read the file at inPath without elevation
|
|
static func canRead(path inPath: String) -> Bool {
|
|
return FileManager.default.isReadableFile(atPath: inPath)
|
|
}
|
|
|
|
// wipes anything in the staging directory. Called on app launch (so a
|
|
// previous session's elevated copies don't survive a relaunch) and on
|
|
// app terminate (so they don't survive a clean quit either). Failure
|
|
// is silent - if cleanup fails the next launch's cleanup will retry.
|
|
static func cleanup() {
|
|
try? FileManager.default.removeItem(at: stagingDirectory)
|
|
}
|
|
|
|
// copies inUrl into the staging directory via sudo, chowns it to the
|
|
// current user, and returns the staged URL. Caller is responsible for
|
|
// catching AdminShell.Error.scriptFailed (cancelled prompt etc).
|
|
//
|
|
// Throws if the parent staging directory can't be created or the
|
|
// admin script fails. Side effect: the system prompts for password
|
|
// the first time within the auth-cache window.
|
|
static func stage(_ inUrl: URL) throws -> URL {
|
|
let vDir = stagingDirectory
|
|
// create as the current user so the dir is owned by us; sudo
|
|
// only handles the file copy itself
|
|
try FileManager.default.createDirectory(
|
|
at: vDir,
|
|
withIntermediateDirectories: true,
|
|
attributes: [.posixPermissions: 0o700]
|
|
)
|
|
// creation attributes don't apply to an existing folder: enforce
|
|
// owner-only access every time (the copies may be other users' files)
|
|
chmod(vDir.path, 0o700)
|
|
|
|
// unique destination file, keeping the original extension so the
|
|
// QLPreviewView / Launch Services can pick the right renderer
|
|
var vDst = vDir.appendingPathComponent(UUID().uuidString)
|
|
let vExt = inUrl.pathExtension
|
|
if !vExt.isEmpty {
|
|
vDst.appendPathExtension(vExt)
|
|
}
|
|
|
|
// Root only *reads* the original; the copy is written by the user
|
|
// (sudo -u ... tee), so root never creates, chowns or chmods a path
|
|
// inside a user-writable folder. A cp + chown + chmod as root could
|
|
// be redirected through a swapped-in symlink / hard link to take
|
|
// ownership of any system file. pipefail makes a failed read fail
|
|
// the script instead of leaving an empty copy behind.
|
|
let vScript = """
|
|
set -o pipefail; \
|
|
/bin/cat \(AdminShell.quote(inUrl.path)) \
|
|
| /usr/bin/sudo -u \(AdminShell.quote(NSUserName())) /usr/bin/tee \(AdminShell.quote(vDst.path)) > /dev/null
|
|
"""
|
|
_ = try AdminShell.run(vScript)
|
|
return vDst
|
|
}
|
|
}
|