mas-cli / mas

:package: Mac App Store command line interface
MIT License
10.87k stars 274 forks source link

`uninstall` does not work on macOS 11.1+ #313

Open Sergong opened 3 years ago

Sergong commented 3 years ago

Your Environment

mas Install Method

Describe the Bug

mas uninstall [id] complains it needs root permissions to uninstall but sudo mas uninstall [id] says the app is not installed.

To Reproduce

Steps to reproduce the behavior:

  1. Install an app; mas install [id]
  2. Uninstall the app with sudo mas uninstall [id]

Expected Behavior

Moves the app to the Trash Bin

Actual Behavior

It will say: Error: not installed

Screenshots, Terminal Output

image

Additional Context

None

endlesslycurious commented 3 years ago

I'm seeing the same issue on MAS 1.7.1 on macOS 11.2

kabirbg commented 3 years ago

Same as @endlesslycurious; at least for the second part it seems like MAS is engineered so that when run as root with sudo (or su) it would look for app store installations by the root user, which would (normally) be none, as opposed to when run with normal user privileges. And macOS probably doesn't allow uninstallation of an app without root password at all... interesting quandary, not sure how you might fix it.

philsherry commented 3 years ago

Works with sudo -s if that helps anyone, but things end up in root's trash:

==> App moved to trash: /private/var/root/.Trash/Telegram.app

baggiponte commented 3 years ago

Same issue here, my mas' version is 1.8.2 and macOS is 11.4.

Stooovie commented 3 years ago

Same in BS 11.6 and mas 1.8.3. Uninstall not working, says "not installed".

JanOwiesniak commented 2 years ago

same issue on:

mas uninstall 408981434 
Warning: Apps installed from the Mac App Store require root permission to remove.
Error: Unable to move app to trash.
Error: Uninstall failed
sudo mas uninstall 408981434
Error: Not installed
sudo -s mas uninstall 408981434
Error: Not installed
mas list
408981434   iMovie      (10.2.5)
carlfugate commented 2 years ago

Same issue

Drallas commented 2 years ago

Same issue for me:

mas uninstall 897118787
Error: Not installed

Any new on what's causing this?

sudo -s doesn't sove it!

whilestevego commented 2 years ago

MacOS 12.2.1 and still not working

image
njjerrysmith commented 2 years ago

Also an issue for me on MAS 1.8.6 for Mac 12.4

yandongxu commented 2 years ago

Same issue for me:

mas uninstall 897118787
Error: Not installed

Any new on what's causing this?

sudo -s doesn't sove it!

Same here!

tarikkavaz commented 1 year ago

Macos 12.6.4 (21G511) Same here

carlfugate commented 1 year ago

I'm not a Swift programmer but I'm guessing something in this function has changed? https://github.com/mas-cli/mas/blob/main/Sources/MasKit/Commands/Uninstall.swift

public func run(_ options: Options) -> Result<Void, MASError> { let appId = UInt64(options.appId)

    guard let product = appLibrary.installedApp(forId: appId) else {
        return .failure(.notInstalled)
    }
claudiodekker commented 1 year ago

It's a permission issue related to macOS's security sandbox : When you use sudo, the command is ran with root privileges and in the root environment, but doesn't have access to your user-specific App Store data. This is why the mas command is unable to see the installed apps and uninstall them. You can verify this by running mas list (will show your apps) and sudo mas list (will be empty).

To fix this, I'm afraid we'll need something like a Privileged Helper Tool (https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/AccessControl.html#//apple_ref/doc/uid/TP40007244-SW5). A working example of this can be found under "EvenBetterAuthorizationSample".

Unfortunately I'm not experienced with Swift, otherwise I'd look into this, but that's basically what's going on.

(related?: https://github.com/mas-cli/mas/issues/417)

BitGrub commented 7 months ago

So mas hasn't had an uninstall function in 3 years?

jacoboneill commented 6 months ago

Still having the same issue

adriangalilea commented 2 months ago

So mas hasn't had an uninstall function in 3 years?

You could have fixed it all along yet here we are 🙃

Unfortunately I'm not experienced with Swift, otherwise I'd look into this, but that's basically what's going on.

I don't think this is the only issue, you can run things with root as your regular user with sudo -u <username> mas list and you'll see it works just fine yet uninstall still fails.

I'm wondering what's the issue, I haven't inspected the code, but can't we just sudo rm -rf "/Applications/<name_of_the_app>.app" and then optionally something like find ~/Library -name "*<name_of_the_app>*" -print0 | xargs -0 sudo rm -rf ?

What am I missing?

rgoldberg commented 1 month ago

The problem is occurring because an earlier part of the code must be run by a user whose associated Apple ID was used to install the app that you want to uninstall (uninstall can be rewritten to skip this part), while a later part must be run as root (thus we cannot avoid needing root access). What follows is a description of what is currently done, why it causes a problem, various solutions to avoid the problem, and my preferred solution:

The folders & files on the file system for apps installed from the App Store (i.e. App Store apps) are owned by the root user, instead of by the user who installed them. An app is uninstalled by trashing its app folder, but that can only be done by root. Thus, no matter what, mas uninstall must have root privileges. The easiest way is to require that the user run sudo mas uninstall <app id>. This ensures that users know that we don’t do anything with their password, and allows the use of /etc/sudoers to allow running without manual password input. Since users shouldn’t be logged in as root, I’ll just talk about sudo from here on out.

The other option is to allow mas uninstall <app id> to be run without sudo, but have it directly ask the user for their password so it can then run sudo in another process or call an equivalent privilege elevation function from Swift. The problems with this are:

Thus, I suggest requiring being called with sudo.

To trash the app, we need to know the path to the app folder on the file system (e.g., /Applications/Xcode.app). Since we only have the app id, we use it to lookup the app’s path from an app list provided by an Apple private framework, which is only populated with apps installed for the Apple ID associated with the current user. The list lookup also limits us to uninstalling apps only for the Apple ID associated with the current user.

sudo mas uninstall <app id> currently fails, obviously, because the current user is therefore root; root is not associated with any Apple ID., so the app list is empty, so we cannot find the app folder path to trash.

If we don’t care about preventing users from uninstalling apps from other Apple IDs, we can try to find the app path without using the list lookup. That would avoid needing mas (or parts thereof) being run as a user with an associated Apple ID that was used to install the app.

Unfortunately, the app ID does not seem to be deterministically findable from the files for an App Store app. The plists I’ve seen under App Store app folders do not include the app ID. Recursively greppjng for the app ID in an app’s files sometime shows it in a binary executable, but sometimes not. One extended attribute (com.apple.appstore.store_cohort) of some App Store app folders includes the app ID (as the value for pgid), but that extended attribute does not include the app ID for some other App Store app folders.

The bundle ID (which should be unique for each app), however, seems to be in Bundle identifier in each app’s Contents/Info.plist (I cannot verify it's there for all, but it's there for all I've seen). So, we could lookup app info (including the bundle ID) from the Apple web API using the app ID, then find the app folder path by searching through the plists for the bundle ID. All as root so we don’t need to switch to any other user.

If we do the above, we should allow the bundle ID to be passed as a command line argument (i.e. mas uninstall <bundle id>) instead of the app ID, so the call to the App Store would be unnecessary in that case. Moreover, we should allow bundle IDs to be used anywhere app IDs are allowed on the command line for already installed apps (e.g., mas upgrade <bundle id>) or to install new apps if the Apple endpoints can take bundle IDs instead of app IDs.

Or we could still use the app list provided by the Apple private framework to get the app path by switching from root to the user that ran sudo, either by switching users within Swift itself just for that one lookup, or by calling sudo -u $SUDO_USER mas path <app id> in a spawned process (mas path <app id> is a new command, but maybe it could be handled by some other new command or by an existing one). I don’t know how hard it will be to implement either of those options. They also might not be able to work, because maybe switching users only switches the “UNIX” user without starting the service or whatever that populates the installed App Store app list.

So, my best guess is:

Do people agree or disagree with this plan?

jacoboneill commented 1 month ago

@rgoldberg I've got to agree your solution seems the most elegant for the problem. Obviously an update to the docs and man pages would need to be done 😄

rgoldberg commented 1 month ago

@jacoboneill Thanks.

I assume I would start just fixing uninstall to work with app ID, then later I'd implement bundle IDs as command line arguments for all appropriate commands at once, just so we could get uninstall fixed a bit faster.

I'm working on some other simpler issues first. I am also hoping to get in touch with some other project members before making larger changes like this, but I might work on this if I don't hear back from others soon.

rgoldberg commented 1 month ago

@chris-araman provided some details about elevated-privilege solutions here: https://github.com/mas-cli/mas/issues/216#issuecomment-985746532

xav-ie commented 2 weeks ago

Hello, I wanted to share my workaround that others may use in the meantime. It is not without issues, but it works for me very well! I am trying to bootstrap my new laptop, and just wanted to declare exactly which mas apps should be installed. Part of doing that requires removing ones that are not declared.

Expand for workaround script 1. what is currently installed? ```zsh ❯ mas list | awk '{print $1}' 682658836 408981434 1501592214 # twingate 409201541 409183694 6446206067 # slack 409203825 ``` ^ In my case, twingate and slack are installed on purpose. The rest are apple defaults I don't want (Garage Band, Pages, etc.) 2. By tacking on the mas apps that should stay installed, we can filter with `uniq -u` and get the ones that should *not* be installed like this: ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214") | sort | uniq -u 408981434 409183694 409201541 409203825 682658836 ``` 3. a. `sudo mas uninstall` each id ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214") | sort | uniq -u \ | xargs -I {} sudo mas uninstall {} Error: Not installed # x5 ``` whoops! https://github.com/mas-cli/mas/issues/313 `mas` should be able to uninstall but it looks like there is some intricate permissions issues. 3. b. workaround using manual method Get the bundleId of each of the applications to uninstall ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214") | sort | uniq -u \ | xargs -I {} curl -s -X GET "https://itunes.apple.com/lookup?id={}" \ | jq -r '.results[0].bundleId' com.apple.iMovieApp com.apple.iWork.Keynote com.apple.iWork.Pages com.apple.iWork.Numbers com.apple.garageband10 ``` 4. Use these bundleIds returns to look up their location on the computer ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214") | sort | uniq -u \ | xargs -I {} curl -s -X GET "https://itunes.apple.com/lookup?id={}" \ | jq -r '.results[0].bundleId' \ | xargs -I {} mdfind "kMDItemCFBundleIdentifier == '{}'" /Applications/iMovie.app /Applications/Keynote.app /Applications/Pages.app /Applications/Numbers.app /Applications/GarageBand.app ``` ^ the benefit of using `mdfind` is that is sidesteps the issue in `mas-cli` as it only searches locations available to the current user. This means, as long as permissions are set up correctly so that *you* cannot see another user's home directory, then their `~/Applications/` will never show up here! We could also apply filtering here to be extra safe, but uncessary. Especially so since I don't plan on having multiple users ever. ...Oooooof. But if you have a Cask installed with `brew`, that would also show up in this list. I don't use `brew`, but you would need to somehow list its install locations and remove those from this list, since those could not have been made by `mas`. 5. uninstall 🎉 ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214") | sort | uniq -u \ | xargs -I {} curl -s -X GET "https://itunes.apple.com/lookup?id={}" \ | jq -r '.results[0].bundleId' \ | xargs -I {} mdfind "kMDItemCFBundleIdentifier == '{}'" \ | xargs -I {} sudo rm -rf {} ``` 6. Bonus: use GNU Parallel to increase uninstall speed Having to download each, then parse each, then remove each sequentially is slow and unnecessary. Using GNU Parallel, we can greatly increase the speed of this to be nearly instantaneous. ```zsh ❯ (mas list | awk '{print $1}'; \ echo -e "6446206067\n1501592214" ) | sort | uniq -u \ | parallel -j $(nproc) ' # Fetch the bundleId using iTunes API bundleId=$(curl -s -X GET "https://itunes.apple.com/lookup?id={}" \ | jq -r ".results[0].bundleId"); # Find the application path using mdfind appPath=$(mdfind "kMDItemCFBundleIdentifier == \"$bundleId\""); # Uninstall the app if found if [ -n "$appPath" ]; then echo "Uninstalling $appPath..."; sudo rm -rf "$appPath"; # Optionally clean up support files sudo rm -rf ~/Library/Preferences/"$bundleId".plist; sudo rm -rf ~/Library/Caches/"$bundleId"; sudo rm -rf ~/Library/Application\ Support/"$bundleId"; else echo "App not found for ID {}"; fi ' ```

Thank you, @rgoldberg, for providing the detailed write up of how to do this. I never would have thought of this. Also, I feel guilty not mentioning bots wrote most the GNU Parallel code

rgoldberg commented 1 week ago

@xav-i.e. Thanks.

To simplify your workaround, you can use kMDItemAppStoreAdamID instead of kMDItemCFBundleIdentifier, so you can skip the bundleId steps.

I'll use NSMetadataQuery to access this information from Swift.