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

Commit b113b2bc authored by Desmond Yao's avatar Desmond Yao Committed by Android (Google) Code Review
Browse files

Merge "Allow non admin user capture bugreport without consent" into main

parents 5cc20fa4 9a64b63c
Loading
Loading
Loading
Loading
+81 −40
Original line number Diff line number Diff line
@@ -40,6 +40,8 @@ import android.os.UserManager;
import android.permission.PermissionManager;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
@@ -56,12 +58,13 @@ import java.util.List;
class PendingReports {
    static final String TAG = IncidentCompanionService.TAG;

    private final Handler mHandler = new Handler();
    private final RequestQueue mRequestQueue = new RequestQueue(mHandler);
    private final RequestQueue mRequestQueue;
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final AppOpsManager mAppOpsManager;
    private final PermissionManager mPermissionManager;
    private final UserManager mUserManager;
    private final Injector mInjector;

    //
    // All fields below must be protected by mLock
@@ -126,14 +129,56 @@ class PendingReports {
        }
    }

    static class Injector {
        private final Context mContext;
        private final Handler mHandler;

        Injector(Context context, Handler handler) {
            mContext = context;
            mHandler = handler;
        }

        public Context getContext() {
            return mContext;
        }

        public Handler getHandler() {
            return mHandler;
        }

        UserManager getUserManager() {
            return UserManager.get(mContext);
        }

        AppOpsManager getAppOpsManager() {
            return mContext.getSystemService(AppOpsManager.class);
        }

        /**
         * Check whether the current user is an admin user, and return the user id if they are.
         * Returns UserHandle.USER_NULL if not valid.
         */
        int getCurrentUserIfAdmin() {
            return IncidentCompanionService.getCurrentUserIfAdmin();
        }
    }

    /**
     * Construct new PendingReports with the context.
     */
    PendingReports(Context context) {
        mContext = context;
        mPackageManager = context.getPackageManager();
        mAppOpsManager = context.getSystemService(AppOpsManager.class);
        mPermissionManager = context.getSystemService(PermissionManager.class);
        this(new Injector(context, new Handler()));
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    PendingReports(Injector injector) {
        mContext = injector.getContext();
        mInjector = injector;
        mRequestQueue = new RequestQueue(injector.getHandler());
        mPackageManager = mContext.getPackageManager();
        mPermissionManager = mContext.getSystemService(PermissionManager.class);
        mAppOpsManager = injector.getAppOpsManager();
        mUserManager = injector.getUserManager();
    }

    /**
@@ -282,28 +327,6 @@ class PendingReports {
            return;
        }

        // Find the current user of the device and check if they are an admin.
        final int currentAdminUser = getCurrentUserIfAdmin();
        final int callingUser = UserHandle.getUserId(callingUid);

        // Deny the report if the current admin user is null
        // or the calling user is not from the same profile group of current user.
        if (currentAdminUser == UserHandle.USER_NULL
                || !isSameProfileGroupUser(callingUser, currentAdminUser)) {
            Log.w(TAG, "Calling user " + callingUser + " doesn't belong to the same profile "
                    + "group of the current admin user " + currentAdminUser);
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }

        // Find the approver app (hint: it's PermissionController).
        final ComponentName receiver = getApproverComponent(currentAdminUser);
        if (receiver == null) {
            // We couldn't find an approver... so deny the request here and now, before we
            // do anything else.
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }
        AttributionSource attributionSource =
                    new AttributionSource.Builder(callingUid)
                            .setPackageName(callingPackage)
@@ -350,6 +373,30 @@ class PendingReports {
            }
        }

        // Find the current user of the device and check if they are an admin.
        final int currentAdminUser = mInjector.getCurrentUserIfAdmin();
        final int callingUser = UserHandle.getUserId(callingUid);

        // Deny the report if the current admin user is null
        // or the calling user is not from the same profile group of current user.
        if (currentAdminUser == UserHandle.USER_NULL
                || !isSameProfileGroupUser(callingUser, currentAdminUser)) {
            Log.w(TAG, "Calling user " + callingUser + " doesn't belong to the same profile "
                    + "group of the current admin user " + currentAdminUser);
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }

        // Find the approver app (hint: it's PermissionController).
        final ComponentName receiver = getApproverComponent(currentAdminUser);
        if (receiver == null) {
            // We couldn't find an approver... so deny the request here and now, before we
            // do anything else.
            Log.w(TAG, "We couldn't find an approver for currentAdminUser " + currentAdminUser);
            denyReportBeforeAddingRec(listener, callingPackage);
            return;
        }

        // Save the record for when the PermissionController comes back to authorize it.
        PendingReportRec rec = null;
        synchronized (mLock) {
@@ -376,10 +423,13 @@ class PendingReports {
     * Cancel a pending report request (because of an explicit call to cancel)
     */
    private void cancelReportImpl(IIncidentAuthListener listener) {
        final int currentAdminUser = getCurrentUserIfAdmin();
        final int currentAdminUser = mInjector.getCurrentUserIfAdmin();
        final ComponentName receiver = getApproverComponent(currentAdminUser);
        if (currentAdminUser != UserHandle.USER_NULL && receiver != null) {
            cancelReportImpl(listener, receiver, currentAdminUser);
        } else {
            Log.w(TAG, "Didn't find exactly approver component for currentAdminUser "
                    + currentAdminUser);
        }
    }

@@ -404,7 +454,7 @@ class PendingReports {
     * cleanup cases to keep the apps' list in sync with ours.
     */
    private void sendBroadcast() {
        final int currentAdminUser = getCurrentUserIfAdmin();
        final int currentAdminUser = mInjector.getCurrentUserIfAdmin();
        if (currentAdminUser == UserHandle.USER_NULL) {
            return;
        }
@@ -481,14 +531,6 @@ class PendingReports {
        }
    }

    /**
     * Check whether the current user is an admin user, and return the user id if they are.
     * Returns UserHandle.USER_NULL if not valid.
     */
    private int getCurrentUserIfAdmin() {
        return IncidentCompanionService.getCurrentUserIfAdmin();
    }

    /**
     * Return the ComponentName of the BroadcastReceiver that will approve reports.
     * The system must have zero or one of these installed.  We only look on the
@@ -530,8 +572,7 @@ class PendingReports {
     */
    private boolean isSameProfileGroupUser(@UserIdInt int currentAdminUser,
            @UserIdInt int callingUser) {
        return UserManager.get(mContext)
                .isSameProfileGroup(currentAdminUser, callingUser);
        return mUserManager.isSameProfileGroup(currentAdminUser, callingUser);
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -5,6 +5,9 @@
    },
    {
      "name": "BugreportManagerTestCases"
    },
    {
      "name": "FrameworksServicesTests_incident"
    }
  ]
}
+10 −0
Original line number Diff line number Diff line
@@ -1095,3 +1095,13 @@ test_module_config {
    ],
    include_filters: ["com.android.server.supervision"],
}

test_module_config {
    name: "FrameworksServicesTests_incident",
    base: "FrameworksServicesTests",
    test_suites: [
        "automotive-tests",
        "device-tests",
    ],
    include_filters: ["com.android.server.incident"],
}
+2 −0
Original line number Diff line number Diff line
# Bug component: 153446
include /cmds/incidentd/OWNERS
+312 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.server.incident;

import static android.Manifest.permission.CAPTURE_CONSENTLESS_BUGREPORT_DELEGATED_CONSENT;
import static android.content.Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED;
import static android.os.IncidentManager.FLAG_CONFIRMATION_DIALOG;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.AppOpsManager;
import android.content.AttributionSourceState;
import android.content.Context;
import android.content.Intent;
import android.content.PermissionChecker;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IIncidentAuthListener;
import android.os.Message;
import android.os.TestLooperManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.PermissionCheckerManager;
import android.permission.PermissionManager;
import android.testing.TestableContext;

import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.quality.Strictness;

import java.util.ArrayList;
import java.util.List;

/**
 * Build/Install/Run: atest FrameworksServicesTests:com.android.server.incident.PendingReportsTest
 */
@RunWith(AndroidJUnit4.class)
public class PendingReportsTest {
    private static final UserInfo ADMIN_USER_INFO =
            new UserInfo(/* id= */ 5678, "adminUser", UserInfo.FLAG_ADMIN);
    private static final UserInfo GUEST_USER_INFO = new UserInfo(/* id= */ 1234, "guestUser", 0);

    public @Rule MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.LENIENT);

    @Rule
    public TestableContext mContext =
            spy(new TestableContext(InstrumentationRegistry.getContext(), null));

    private final IIncidentAuthListener mIncidentAuthListener = mock(IIncidentAuthListener.class);

    private PendingReports mPendingReports;
    private TestInjector mTestInjector;
    private HandlerThread mUiThread;
    private TestLooperManager mTestLooperManager;
    @Mock private PackageManager mMockPackageManager;
    @Mock private UserManager mMockUserManager;
    @Mock private AppOpsManager mMockAppOpsManager;
    @Mock private PermissionCheckerManager mPermissionCheckerManager;
    @Mock private IBinder mListenerBinder;

    public class TestInjector extends PendingReports.Injector {

        private int mUserId = ADMIN_USER_INFO.id;

        TestInjector(Context context, Handler handler) {
            super(context, handler);
        }

        @Override
        UserManager getUserManager() {
            return mMockUserManager;
        }

        @Override
        AppOpsManager getAppOpsManager() {
            return mMockAppOpsManager;
        }

        @Override
        int getCurrentUserIfAdmin() {
            return mUserId;
        }

        void clearAdminUserId() {
            mUserId = UserHandle.USER_NULL;
        }
    }

    @Before
    public void setup() throws Exception {
        // generate new IBinder instance every time for test
        when(mIncidentAuthListener.asBinder()).thenReturn(mListenerBinder);

        ResolveInfo resolveInfo = new ResolveInfo();
        ActivityInfo mincidentActivityInfo = new ActivityInfo();
        mincidentActivityInfo.name = "PendingReportsTest";
        mincidentActivityInfo.packageName = mContext.getPackageName();
        resolveInfo.activityInfo = mincidentActivityInfo;

        List<ResolveInfo> intentReceivers = new ArrayList<>();
        intentReceivers.add(resolveInfo);
        ArgumentMatcher<Intent> filterIntent =
                intent -> intent.getAction().equals(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
        when(mMockPackageManager.queryBroadcastReceiversAsUser(
                        argThat(filterIntent),
                        /* flags= */ anyInt(),
                        /* userId= */ eq(ADMIN_USER_INFO.id)))
                .thenReturn(intentReceivers);
        mContext.setMockPackageManager(mMockPackageManager);

        when(mMockUserManager.isSameProfileGroup(anyInt(), eq(ADMIN_USER_INFO.id)))
                .thenReturn(true);

        mUiThread = new HandlerThread("MockUiThread");
        mUiThread.start();
        mTestLooperManager =
                InstrumentationRegistry.getInstrumentation()
                        .acquireLooperManager(mUiThread.getLooper());

        doReturn(Context.PERMISSION_CHECKER_SERVICE)
                .when(mContext)
                .getSystemServiceName(PermissionCheckerManager.class);
        mContext.addMockSystemService(PermissionCheckerManager.class, mPermissionCheckerManager);
        mContext.addMockSystemService(
                Context.PERMISSION_CHECKER_SERVICE, mPermissionCheckerManager);
        mContext.addMockSystemService(PermissionManager.class, new PermissionManager(mContext));

        Handler testHandler = new Handler(mUiThread.getLooper());
        mTestInjector = new TestInjector(mContext, testHandler);
        mPendingReports = new PendingReports(mTestInjector);
        mPendingReports.onBootCompleted();
    }

    @After
    public void tearDown() {
        mTestLooperManager.release();
        mUiThread.quit();
    }

    @Test
    public void testAuthorizeReport_sendsIncidentBroadcast() throws Exception {
        mockDelegatePermissionStatus(false);
        mPendingReports.authorizeReport(
                ADMIN_USER_INFO.id,
                mContext.getPackageName(),
                "receiverClass",
                "report_id",
                FLAG_CONFIRMATION_DIALOG,
                mIncidentAuthListener);
        drainRequestQueue();

        assertThat(mPendingReports.getPendingReports()).hasSize(1);
        assertThat(mPendingReports.getPendingReports().get(0))
                .matches(
                        "content://android\\.os\\.IncidentManager/pending\\?id=1"
                                + "&pkg=com\\.android\\.frameworks\\.servicestests"
                                + "&flags=1"
                                + "&t=(\\d+)"
                                + "&receiver=receiverClass&r=report_id");
        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mContext)
                .sendBroadcastAsUser(
                        intentArgumentCaptor.capture(),
                        /* user= */ any(),
                        /* receiverPermission= */ eq(
                                android.Manifest.permission.APPROVE_INCIDENT_REPORTS),
                        /* options= */ any());
        assertBroadcastHasExpectedValue(intentArgumentCaptor.getValue());
    }

    private void assertBroadcastHasExpectedValue(Intent intent) {
        assertThat(intent.getAction()).isEqualTo(ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
        assertThat(intent.getComponent().getClassName()).isEqualTo("PendingReportsTest");
        assertThat(intent.getComponent().getPackageName()).isEqualTo(mContext.getPackageName());
    }

    @Test
    public void testAuthorizeReport_nonAdmin_getsApprovedIfHaveConsentlessPermission()
            throws Exception {
        mockDelegatePermissionStatus(true);
        mTestInjector.clearAdminUserId();

        mPendingReports.authorizeReport(
                GUEST_USER_INFO.id,
                mContext.getPackageName(),
                "receiverClass",
                "report_id",
                FLAG_CONFIRMATION_DIALOG,
                mIncidentAuthListener);
        drainRequestQueue();

        verify(mIncidentAuthListener).onReportApproved();
    }

    @Test
    public void testAuthorizeReport_nonAdmin_denysByDefault() throws Exception {
        mockDelegatePermissionStatus(false);
        mTestInjector.clearAdminUserId();

        mPendingReports.authorizeReport(
                GUEST_USER_INFO.id,
                mContext.getPackageName(),
                "receiverClass",
                "report_id",
                FLAG_CONFIRMATION_DIALOG,
                mIncidentAuthListener);
        drainRequestQueue();

        verify(mIncidentAuthListener).onReportDenied();
    }

    @Test
    public void testCancelAuthorization_sendsIncidentBroadcast() throws Exception {
        mockDelegatePermissionStatus(false);
        mPendingReports.authorizeReport(
                ADMIN_USER_INFO.id,
                mContext.getPackageName(),
                "receiverClass",
                "report_id",
                FLAG_CONFIRMATION_DIALOG,
                mIncidentAuthListener);
        drainRequestQueue();
        mPendingReports.cancelAuthorization(mIncidentAuthListener);
        drainRequestQueue();

        assertThat(mPendingReports.getPendingReports()).isEmpty();
        ArgumentCaptor<Intent> intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
        verify(mContext, times(2))
                .sendBroadcastAsUser(
                        /* intent= */ intentArgumentCaptor.capture(),
                        /* user= */ any(),
                        /* receiverPermission= */ eq(
                                android.Manifest.permission.APPROVE_INCIDENT_REPORTS),
                        /* options= */ any());
        List<Intent> intents = intentArgumentCaptor.getAllValues();
        assertThat(intents).hasSize(2);
        // authorize and cancel sends same intent
        assertBroadcastHasExpectedValue(intents.get(0));
        assertBroadcastHasExpectedValue(intents.get(1));
    }

    private void mockDelegatePermissionStatus(boolean granted) {
        int permissionCode =
                granted
                        ? PermissionChecker.PERMISSION_GRANTED
                        : PermissionChecker.PERMISSION_HARD_DENIED;
        doReturn(permissionCode)
                .when(mPermissionCheckerManager)
                .checkPermission(
                        eq(CAPTURE_CONSENTLESS_BUGREPORT_DELEGATED_CONSENT),
                        any(AttributionSourceState.class),
                        isNull(),
                        anyBoolean(),
                        anyBoolean(),
                        anyBoolean(),
                        anyInt());
    }

    private void drainRequestQueue() {
        while (true) {
            Message m = mTestLooperManager.poll();
            if (m == null) {
                break;
            }
            mTestLooperManager.execute(m);
            mTestLooperManager.recycle(m);
        }
    }
}
Loading