Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 215e074b authored by Sumedh Sen's avatar Sumedh Sen
Browse files

Check for restrictions and permissions before installing

Check whether the caller has install permissions, if any user
restriction is set on the user by device owner or system and exit the
installation early be showing appropriate dialogs and setting correct
results.

Bug: 182205982
Test: builds successfully
Test: No CTS Tests. Flag to use new app is turned off by default

Change-Id: I305f9705576e1980d3068f648e25081f1b666c7f
parent 2277a971
Loading
Loading
Loading
Loading
+108 −0
Original line number Diff line number Diff line
@@ -16,13 +16,27 @@

package com.android.packageinstaller.v2.model;

import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;

import android.Manifest;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import androidx.annotation.Nullable;
import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;

@@ -32,6 +46,8 @@ public class InstallRepository {
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final PackageInstaller mPackageInstaller;
    private final UserManager mUserManager;
    private final DevicePolicyManager mDevicePolicyManager;
    private final boolean mLocalLOGV = false;
    private Intent mIntent;
    private boolean mIsSessionInstall;
@@ -47,6 +63,8 @@ public class InstallRepository {
        mContext = context;
        mPackageManager = context.getPackageManager();
        mPackageInstaller = mPackageManager.getPackageInstaller();
        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
        mUserManager = context.getSystemService(UserManager.class);
    }

    /**
@@ -56,6 +74,7 @@ public class InstallRepository {
     * @param intent the incoming {@link Intent} object for installing a package
     * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName
     * @return
     * <p>{@link InstallAborted} if there are errors while performing the checks</p>
     * <p>{@link InstallStaging} after successfully performing the checks</p>
     */
    public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) {
@@ -85,10 +104,99 @@ public class InstallRepository {
        if (mCallingUid == Process.INVALID_UID) {
            Log.e(TAG, "Could not determine the launching uid.");
        }
        final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage);
        // Uid of the source package, with a preference to uid from ApplicationInfo
        final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid;

        if (mCallingUid == Process.INVALID_UID && sourceInfo == null) {
            // Caller's identity could not be determined. Abort the install
            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
        }

        if (!isCallerSessionOwner(mPackageInstaller, originatingUid, mSessionId)) {
            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
        }

        mIsTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, mIntent, originatingUid);

        if (!isInstallPermissionGrantedOrRequested(mContext, mCallingUid, originatingUid,
            mIsTrustedSource)) {
            return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
        }

        String restriction = getDevicePolicyRestrictions();
        if (restriction != null) {
            InstallAborted.Builder abortedBuilder =
                new InstallAborted.Builder(ABORT_REASON_POLICY).setMessage(restriction);
            final Intent adminSupportDetailsIntent =
                mDevicePolicyManager.createAdminSupportIntent(restriction);
            if (adminSupportDetailsIntent != null) {
                abortedBuilder.setResultIntent(adminSupportDetailsIntent);
            }
            return abortedBuilder.build();
        }

        maybeRemoveInvalidInstallerPackageName(callerInfo);

        return new InstallStaging();
    }

    /**
     * @return the ApplicationInfo for the installation source (the calling package), if available
     */
    @Nullable
    private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
        if (callingPackage == null) {
            return null;
        }
        try {
            return mPackageManager.getApplicationInfo(callingPackage, 0);
        } catch (PackageManager.NameNotFoundException ignored) {
            return null;
        }
    }

    private boolean isInstallRequestFromTrustedSource(ApplicationInfo sourceInfo, Intent intent,
        int originatingUid) {
        boolean isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
        return sourceInfo != null && sourceInfo.isPrivilegedApp()
            && (isNotUnknownSource
            || isPermissionGranted(mContext, Manifest.permission.INSTALL_PACKAGES, originatingUid));
    }

    private String getDevicePolicyRestrictions() {
        final String[] restrictions = new String[]{
            UserManager.DISALLOW_INSTALL_APPS,
            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
            UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
        };

        for (String restriction : restrictions) {
            if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
                continue;
            }
            return restriction;
        }
        return null;
    }

    private void maybeRemoveInvalidInstallerPackageName(CallerInfo callerInfo) {
        final String installerPackageNameFromIntent =
            mIntent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
        if (installerPackageNameFromIntent == null) {
            return;
        }
        if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.getPackageName())
            && !isPermissionGranted(mPackageManager, Manifest.permission.INSTALL_PACKAGES,
            callerInfo.getPackageName())) {
            Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
                + " is invalid. Remove it.");
            EventLog.writeEvent(0x534e4554, "236687884", callerInfo.getUid(),
                "Invalid EXTRA_INSTALLER_PACKAGE_NAME");
            mIntent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
        }
    }

    public static class CallerInfo {

        private final String mPackageName;
+189 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.packageinstaller.v2.model;

import android.Manifest;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.Arrays;

public class PackageUtil {

    private static final String TAG = InstallRepository.class.getSimpleName();
    private static final String DOWNLOADS_AUTHORITY = "downloads";

    /**
     * Determines if the UID belongs to the system downloads provider and returns the
     * {@link ApplicationInfo} of the provider
     *
     * @param uid UID of the caller
     * @return {@link ApplicationInfo} of the provider if a downloads provider exists, it is a
     *     system app, and its UID matches with the passed UID, null otherwise.
     */
    public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) {
        final ProviderInfo providerInfo = pm.resolveContentProvider(
            DOWNLOADS_AUTHORITY, 0);
        if (providerInfo == null) {
            // There seems to be no currently enabled downloads provider on the system.
            return null;
        }
        ApplicationInfo appInfo = providerInfo.applicationInfo;
        if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) {
            return appInfo;
        }
        return null;
    }

    /**
     * Get the maximum target sdk for a UID.
     *
     * @param context The context to use
     * @param uid The UID requesting the install/uninstall
     * @return The maximum target SDK or -1 if the uid does not match any packages.
     */
    public static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) {
        PackageManager pm = context.getPackageManager();
        final String[] packages = pm.getPackagesForUid(uid);
        int targetSdkVersion = -1;
        if (packages != null) {
            for (String packageName : packages) {
                try {
                    ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
                    targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore and try the next package
                }
            }
        }
        return targetSdkVersion;
    }

    /**
     * @param context the {@link Context} object
     * @param permission the permission name to check
     * @param callingUid the UID of the caller who's permission is being checked
     * @return {@code true} if the callingUid is granted the said permission
     */
    public static boolean isPermissionGranted(Context context, String permission, int callingUid) {
        return context.checkPermission(permission, -1, callingUid)
            == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * @param pm the {@link PackageManager} object
     * @param permission the permission name to check
     * @param packageName the name of the package who's permission is being checked
     * @return {@code true} if the package is granted the said permission
     */
    public static boolean isPermissionGranted(PackageManager pm, String permission,
        String packageName) {
        return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * @param context the {@link Context} object
     * @param callingUid the UID of the caller who's permission is being checked
     * @param originatingUid the UID from where install is being originated. This could be same as
     * callingUid or it will be the UID of the package performing a session based install
     * @param isTrustedSource whether install request is coming from a privileged app or an app that
     * has {@link Manifest.permission.INSTALL_PACKAGES} permission granted
     * @return {@code true} if the package is granted the said permission
     */
    public static boolean isInstallPermissionGrantedOrRequested(Context context, int callingUid,
        int originatingUid, boolean isTrustedSource) {
        boolean isDocumentsManager =
            isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid);
        boolean isSystemDownloadsProvider =
            getSystemDownloadsProviderInfo(context.getPackageManager(), callingUid) != null;

        if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {

            final int targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid);
            if (targetSdkVersion < 0) {
                // Invalid originating uid supplied. Abort install.
                Log.w(TAG, "Cannot get target sdk version for uid " + originatingUid);
                return false;
            } else if (targetSdkVersion >= Build.VERSION_CODES.O
                && !isUidRequestingPermission(context.getPackageManager(), originatingUid,
                Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
                Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
                    + Manifest.permission.REQUEST_INSTALL_PACKAGES);
                return false;
            }
        }
        return true;
    }

    /**
     * @param pm the {@link PackageManager} object
     * @param uid the UID of the caller who's permission is being checked
     * @param permission the permission name to check
     * @return {@code true} if the caller is requesting the said permission in its Manifest
     */
    public static boolean isUidRequestingPermission(PackageManager pm, int uid, String permission) {
        final String[] packageNames = pm.getPackagesForUid(uid);
        if (packageNames == null) {
            return false;
        }
        for (final String packageName : packageNames) {
            final PackageInfo packageInfo;
            try {
                packageInfo = pm.getPackageInfo(packageName,
                    PackageManager.GET_PERMISSIONS);
            } catch (PackageManager.NameNotFoundException e) {
                // Ignore and try the next package
                continue;
            }
            if (packageInfo.requestedPermissions != null
                && Arrays.asList(packageInfo.requestedPermissions).contains(permission)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param pi the {@link PackageInstaller} object to use
     * @param originatingUid the UID of the package performing a session based install
     * @param sessionId ID of the install session
     * @return {@code true} if the caller is the session owner
     */
    public static boolean isCallerSessionOwner(PackageInstaller pi, int originatingUid,
        int sessionId) {
        if (sessionId == SessionInfo.INVALID_ID) {
            return false;
        }
        if (originatingUid == Process.ROOT_UID) {
            return true;
        }
        PackageInstaller.SessionInfo sessionInfo = pi.getSessionInfo(sessionId);
        if (sessionInfo == null) {
            return false;
        }
        int installerUid = sessionInfo.getInstallerUid();
        return originatingUid == installerUid;
    }
}
+98 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.packageinstaller.v2.model.installstagedata;


import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class InstallAborted extends InstallStage {

    public static final int ABORT_REASON_INTERNAL_ERROR = 0;
    public static final int ABORT_REASON_POLICY = 1;
    private final int mStage = InstallStage.STAGE_ABORTED;
    private final int mAbortReason;

    /**
     * It will hold the restriction name, when the restriction was enforced by the system, and not
     * a device admin.
     */
    @NonNull
    private final String mMessage;
    /**
     * <p>If abort reason is ABORT_REASON_POLICY, then this will hold the Intent
     * to display a support dialog when a feature was disabled by an admin. It will be
     * {@code null} if the feature is disabled by the system. In this case, the restriction name
     * will be set in {@link #mMessage} </p>
     *
     * <p>If the abort reason is ABORT_REASON_INTERNAL_ERROR, it <b>may</b> hold an
     * intent to be sent as a result to the calling activity.</p>
     */
    @Nullable
    private final Intent mIntent;

    private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent) {
        mAbortReason = reason;
        mMessage = message;
        mIntent = intent;
    }

    public int getAbortReason() {
        return mAbortReason;
    }

    @NonNull
    public String getMessage() {
        return mMessage;
    }

    @Nullable
    public Intent getResultIntent() {
        return mIntent;
    }

    @Override
    public int getStageCode() {
        return mStage;
    }

    public static class Builder {

        private final int mAbortReason;
        private String mMessage = "";
        private Intent mIntent = null;

        public Builder(int reason) {
            mAbortReason = reason;
        }

        public Builder setMessage(@NonNull String message) {
            mMessage = message;
            return this;
        }

        public Builder setResultIntent(@NonNull Intent intent) {
            mIntent = intent;
            return this;
        }

        public InstallAborted build() {
            return new InstallAborted(mAbortReason, mMessage, mIntent);
        }
    }
}
+103 −0
Original line number Diff line number Diff line
@@ -17,16 +17,26 @@
package com.android.packageinstaller.v2.ui;

import static android.os.Process.INVALID_UID;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;

import android.content.Intent;
import android.os.Bundle;
import android.os.UserManager;
import android.util.Log;
import android.view.Window;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import com.android.packageinstaller.R;
import com.android.packageinstaller.v2.model.InstallRepository;
import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment;
import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment;
import com.android.packageinstaller.v2.viewmodel.InstallViewModel;
import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory;

@@ -37,15 +47,20 @@ public class InstallLaunch extends FragmentActivity {
    public static final String EXTRA_CALLING_PKG_NAME =
            InstallLaunch.class.getPackageName() + ".callingPkgName";
    private static final String TAG = InstallLaunch.class.getSimpleName();
    private static final String TAG_DIALOG = "dialog";
    private final boolean mLocalLOGV = false;
    private InstallViewModel mInstallViewModel;
    private InstallRepository mInstallRepository;

    private FragmentManager mFragmentManager;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.requestWindowFeature(Window.FEATURE_NO_TITLE);

        mFragmentManager = getSupportFragmentManager();
        mInstallRepository = new InstallRepository(getApplicationContext());
        mInstallViewModel = new ViewModelProvider(this,
                new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get(
@@ -64,5 +79,93 @@ public class InstallLaunch extends FragmentActivity {
     * Main controller of the UI. This method shows relevant dialogs based on the install stage
     */
    private void onInstallStageChange(InstallStage installStage) {
        if (installStage.getStageCode() == InstallStage.STAGE_STAGING) {
            InstallStagingFragment stagingDialog = new InstallStagingFragment();
            showDialogInner(stagingDialog);
        } else if (installStage.getStageCode() == InstallStage.STAGE_ABORTED) {
            InstallAborted aborted = (InstallAborted) installStage;
            switch (aborted.getAbortReason()) {
                // TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR
                case ABORT_REASON_INTERNAL_ERROR -> setResult(RESULT_CANCELED, true);
                case ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
                default -> setResult(RESULT_CANCELED, true);
            }
        } else {
            Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode());
            showDialogInner(null);
        }
    }

    private void showPolicyRestrictionDialog(InstallAborted aborted) {
        String restriction = aborted.getMessage();
        Intent adminSupportIntent = aborted.getResultIntent();
        boolean shouldFinish;

        // If the given restriction is set by an admin, display information about the
        // admin enforcing the restriction for the affected user. If not enforced by the admin,
        // show the system dialog.
        if (adminSupportIntent != null) {
            if (mLocalLOGV) {
                Log.i(TAG, "Restriction set by admin, starting " + adminSupportIntent);
            }
            startActivity(adminSupportIntent);
            // Finish the package installer app since the next dialog will not be shown by this app
            shouldFinish = true;
        } else {
            if (mLocalLOGV) {
                Log.i(TAG, "Restriction set by system: " + restriction);
            }
            DialogFragment blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction);
            // Don't finish the package installer app since the next dialog
            // will be shown by this app
            shouldFinish = false;
            showDialogInner(blockedByPolicyDialog);
        }
        setResult(RESULT_CANCELED, shouldFinish);
    }

    /**
     * Create a new dialog based on the install restriction enforced.
     *
     * @param restriction The restriction to create the dialog for
     * @return The dialog
     */
    private DialogFragment createDevicePolicyRestrictionDialog(String restriction) {
        if (mLocalLOGV) {
            Log.i(TAG, "createDialog(" + restriction + ")");
        }
        return switch (restriction) {
            case UserManager.DISALLOW_INSTALL_APPS ->
                new SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text);
            case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
                new SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text);
            default -> null;
        };
    }

    /**
     * Replace any visible dialog by the dialog returned by InstallRepository
     *
     * @param newDialog The new dialog to display
     */
    private void showDialogInner(@Nullable DialogFragment newDialog) {
        DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
            TAG_DIALOG);
        if (currentDialog != null) {
            currentDialog.dismissAllowingStateLoss();
        }
        if (newDialog != null) {
            newDialog.show(mFragmentManager, TAG_DIALOG);
        }
    }

    public void setResult(int resultCode, boolean shouldFinish) {
        // TODO: This is incomplete. We need to send RESULT_FIRST_USER, RESULT_OK etc
        //  for relevant use cases. Investigate when to send what result.
        super.setResult(resultCode);
        if (shouldFinish) {
            finish();
        }
    }
}
+49 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading