blob: adcda0a631520161ce56d13c38a26ec22e7b67cd [file] [log] [blame]
/*
* Copyright (C) 2019 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.permission.PermissionManager.PERMISSION_GRANTED;
import android.Manifest;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.IIncidentAuthListener;
import android.os.IncidentManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.PermissionManager;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
// TODO: User changes should deny everything that's pending.
/**
* Tracker for reports pending approval.
*/
class PendingReports {
static final String TAG = IncidentCompanionService.TAG;
private final Handler mHandler = new Handler();
private final RequestQueue mRequestQueue = new RequestQueue(mHandler);
private final Context mContext;
private final PackageManager mPackageManager;
private final AppOpsManager mAppOpsManager;
private final PermissionManager mPermissionManager;
//
// All fields below must be protected by mLock
//
private final Object mLock = new Object();
private final ArrayList<PendingReportRec> mPending = new ArrayList();
/**
* The next ID we'll use when we make a PendingReportRec.
*/
private int mNextPendingId = 1;
/**
* One for each authorization that's pending.
*/
private final class PendingReportRec {
public int id;
public String callingPackage;
public int flags;
public IIncidentAuthListener listener;
public long addedRealtime;
public long addedWalltime;
public String receiverClass;
public String reportId;
/**
* Construct a PendingReportRec, with an auto-incremented id.
*/
PendingReportRec(String callingPackage, String receiverClass, String reportId, int flags,
IIncidentAuthListener listener) {
this.id = mNextPendingId++;
this.callingPackage = callingPackage;
this.flags = flags;
this.listener = listener;
this.addedRealtime = SystemClock.elapsedRealtime();
this.addedWalltime = System.currentTimeMillis();
this.receiverClass = receiverClass;
this.reportId = reportId;
}
/**
* Get the Uri that contains the flattened data.
*/
Uri getUri() {
final Uri.Builder builder = (new Uri.Builder())
.scheme(IncidentManager.URI_SCHEME)
.authority(IncidentManager.URI_AUTHORITY)
.path(IncidentManager.URI_PATH)
.appendQueryParameter(IncidentManager.URI_PARAM_ID, Integer.toString(id))
.appendQueryParameter(IncidentManager.URI_PARAM_CALLING_PACKAGE, callingPackage)
.appendQueryParameter(IncidentManager.URI_PARAM_FLAGS, Integer.toString(flags))
.appendQueryParameter(IncidentManager.URI_PARAM_TIMESTAMP,
Long.toString(addedWalltime));
if (receiverClass != null && receiverClass.length() > 0) {
builder.appendQueryParameter(IncidentManager.URI_PARAM_RECEIVER_CLASS,
receiverClass);
}
if (reportId != null && reportId.length() > 0) {
builder.appendQueryParameter(IncidentManager.URI_PARAM_REPORT_ID, reportId);
}
return builder.build();
}
}
/**
* Construct new PendingReports with the context.
*/
PendingReports(Context context) {
mContext = context;
mPackageManager = context.getPackageManager();
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mPermissionManager = context.getSystemService(PermissionManager.class);
}
/**
* ONEWAY binder call to initiate authorizing the report. The actual logic is posted
* to mRequestQueue, and may happen later.
* <p>
* The security checks are handled by IncidentCompanionService.
*/
public void authorizeReport(int callingUid, final String callingPackage,
final String receiverClass, final String reportId, final int flags,
final IIncidentAuthListener listener) {
// Starting the system server is complicated, and rather than try to
// have a complicated lifecycle that we share with dumpstated and incidentd,
// we will accept the request, and then display it whenever it becomes possible to.
mRequestQueue.enqueue(listener.asBinder(), true, () -> {
authorizeReportImpl(callingUid, callingPackage, receiverClass, reportId,
flags, listener);
});
}
/**
* ONEWAY binder call to cancel the inbound authorization request.
* <p>
* This is a oneway call, and so is authorizeReport, so the
* caller's ordering is preserved. The other calls on this object are synchronous, so
* their ordering is not guaranteed with respect to these calls. So the implementation
* sends out extra broadcasts to allow for eventual consistency.
* <p>
* The security checks are handled by IncidentCompanionService.
*/
public void cancelAuthorization(final IIncidentAuthListener listener) {
mRequestQueue.enqueue(listener.asBinder(), false, () -> {
cancelReportImpl(listener);
});
}
/**
* SYNCHRONOUS binder call to get the list of reports that are pending confirmation
* by the user.
* <p>
* The security checks are handled by IncidentCompanionService.
*/
public List<String> getPendingReports() {
synchronized (mLock) {
final int size = mPending.size();
final ArrayList<String> result = new ArrayList(size);
for (int i = 0; i < size; i++) {
result.add(mPending.get(i).getUri().toString());
}
return result;
}
}
/**
* SYNCHRONOUS binder call to mark a report as approved.
* <p>
* The security checks are handled by IncidentCompanionService.
*/
public void approveReport(String uri) {
final PendingReportRec rec;
synchronized (mLock) {
rec = findAndRemovePendingReportRecLocked(uri);
if (rec == null) {
Log.e(TAG, "confirmApproved: Couldn't find record for uri: " + uri);
return;
}
}
// Re-do the broadcast, so whoever is listening knows the list changed,
// in case another one was added in the meantime.
sendBroadcast();
Log.i(TAG, "Approved report: " + uri);
try {
rec.listener.onReportApproved();
} catch (RemoteException ex) {
Log.w(TAG, "Failed calling back for approval for: " + uri, ex);
}
}
/**
* SYNCHRONOUS binder call to mark a report as NOT approved.
*/
public void denyReport(String uri) {
final PendingReportRec rec;
synchronized (mLock) {
rec = findAndRemovePendingReportRecLocked(uri);
if (rec == null) {
Log.e(TAG, "confirmDenied: Couldn't find record for uri: " + uri);
return;
}
}
// Re-do the broadcast, so whoever is listening knows the list changed,
// in case another one was added in the meantime.
sendBroadcast();
Log.i(TAG, "Denied report: " + uri);
try {
rec.listener.onReportDenied();
} catch (RemoteException ex) {
Log.w(TAG, "Failed calling back for denial for: " + uri, ex);
}
}
/**
* Implementation of adb shell dumpsys debugreportcompanion.
*/
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
if (args.length == 0) {
// Standard text dumpsys
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
synchronized (mLock) {
final int size = mPending.size();
writer.println("mPending: (" + size + ")");
for (int i = 0; i < size; i++) {
final PendingReportRec entry = mPending.get(i);
writer.println(String.format(" %11d %s: %s", entry.addedRealtime,
df.format(new Date(entry.addedWalltime)),
entry.getUri().toString()));
}
}
}
}
/**
* Handle the boot process... Starts everything running once the system is
* up enough for us to do UI.
*/
public void onBootCompleted() {
// Release the enqueued work.
mRequestQueue.start();
}
/**
* Start the confirmation process.
*/
private void authorizeReportImpl(int callingUid, final String callingPackage,
final String receiverClass, final String reportId,
int flags, final IIncidentAuthListener listener) {
// Enforce that the calling package pertains to the callingUid.
if (callingUid != 0 && !isPackageInUid(callingUid, callingPackage)) {
Log.w(TAG, "Calling uid " + callingUid + " doesn't match package "
+ callingPackage);
denyReportBeforeAddingRec(listener, callingPackage);
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;
}
// Only with userdebug/eng build: it could check capture consentless bugreport permission
// and approve the report when it's granted.
boolean captureConsentlessBugreportOnUserdebugBuildGranted = false;
if ((Build.IS_USERDEBUG || Build.IS_ENG)
&& (flags & IncidentManager.FLAG_ALLOW_CONSENTLESS_BUGREPORT) != 0) {
AttributionSource attributionSource =
new AttributionSource.Builder(callingUid)
.setPackageName(callingPackage)
.build();
captureConsentlessBugreportOnUserdebugBuildGranted =
mPermissionManager.checkPermissionForDataDelivery(
Manifest.permission.CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
attributionSource,
/* message= */ null)
== PERMISSION_GRANTED;
}
if (captureConsentlessBugreportOnUserdebugBuildGranted) {
try {
PendingReportRec rec =
new PendingReportRec(
callingPackage, receiverClass, reportId, flags, listener);
Log.d(TAG, "approving consentless report: " + rec.getUri());
listener.onReportApproved();
return;
} catch (RemoteException e) {
Log.e(TAG, "authorizeReportImpl listener.onReportApproved RemoteException: ", e);
}
}
// Save the record for when the PermissionController comes back to authorize it.
PendingReportRec rec = null;
synchronized (mLock) {
rec = new PendingReportRec(callingPackage, receiverClass, reportId, flags, listener);
mPending.add(rec);
}
try {
listener.asBinder().linkToDeath(() -> {
Log.i(TAG, "Got death notification listener=" + listener);
cancelReportImpl(listener, receiver, currentAdminUser);
}, 0);
} catch (RemoteException ex) {
Log.e(TAG, "Remote died while trying to register death listener: " + rec.getUri());
// First, remove from our list.
cancelReportImpl(listener, receiver, currentAdminUser);
}
// Go tell Permission controller to start asking the user.
sendBroadcast(receiver, currentAdminUser);
}
/**
* Cancel a pending report request (because of an explicit call to cancel)
*/
private void cancelReportImpl(IIncidentAuthListener listener) {
final int currentAdminUser = getCurrentUserIfAdmin();
final ComponentName receiver = getApproverComponent(currentAdminUser);
if (currentAdminUser != UserHandle.USER_NULL && receiver != null) {
cancelReportImpl(listener, receiver, currentAdminUser);
}
}
/**
* Cancel a pending report request (either because of an explicit call to cancel
* by the calling app, or because of a binder death).
*/
private void cancelReportImpl(IIncidentAuthListener listener, ComponentName receiver,
@UserIdInt int user) {
// First, remove from our list.
synchronized (mLock) {
removePendingReportRecLocked(listener);
}
// Second, call back to PermissionController to say it's canceled.
sendBroadcast(receiver, user);
}
/**
* Send an extra copy of the broadcast, to tell them that the list has changed
* because of an addition or removal. This function is less aggressive than
* authorizeReportImpl in logging about failures, because this is for use in
* cleanup cases to keep the apps' list in sync with ours.
*/
private void sendBroadcast() {
final int currentAdminUser = getCurrentUserIfAdmin();
if (currentAdminUser == UserHandle.USER_NULL) {
return;
}
final ComponentName receiver = getApproverComponent(currentAdminUser);
if (receiver == null) {
return;
}
sendBroadcast(receiver, currentAdminUser);
}
/**
* Send the confirmation broadcast.
*/
private void sendBroadcast(ComponentName receiver, int currentUser) {
final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
intent.setComponent(receiver);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setBackgroundActivityStartsAllowed(true);
// Send it to the current user.
mContext.sendBroadcastAsUser(intent, UserHandle.of(currentUser),
android.Manifest.permission.APPROVE_INCIDENT_REPORTS, options.toBundle());
}
/**
* Remove a PendingReportRec keyed by uri, and return it.
*/
private PendingReportRec findAndRemovePendingReportRecLocked(String uriString) {
final Uri uri = Uri.parse(uriString);
final int id;
try {
final String idStr = uri.getQueryParameter(IncidentManager.URI_PARAM_ID);
id = Integer.parseInt(idStr);
} catch (NumberFormatException ex) {
Log.w(TAG, "Can't parse id from: " + uriString);
return null;
}
for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
final PendingReportRec rec = i.next();
if (rec.id == id) {
i.remove();
return rec;
}
}
return null;
}
/**
* Remove a PendingReportRec keyed by listener.
*/
private void removePendingReportRecLocked(IIncidentAuthListener listener) {
for (Iterator<PendingReportRec> i = mPending.iterator(); i.hasNext();) {
final PendingReportRec rec = i.next();
if (rec.listener.asBinder() == listener.asBinder()) {
Log.i(TAG, " ...Removed PendingReportRec index=" + i + ": " + rec.getUri());
i.remove();
}
}
}
/**
* Just call listener.deny() (wrapping the RemoteException), without try to
* add it to the list.
*/
private void denyReportBeforeAddingRec(IIncidentAuthListener listener, String pkg) {
try {
listener.onReportDenied();
} catch (RemoteException ex) {
Log.w(TAG, "Failed calling back for denial for " + pkg, ex);
}
}
/**
* 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
* system partition. When the broadcast happens, the component will also need
* have the APPROVE_INCIDENT_REPORTS permission.
*/
private ComponentName getApproverComponent(int userId) {
// Find the one true BroadcastReceiver
final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
final List<ResolveInfo> matches = mPackageManager.queryBroadcastReceiversAsUser(intent,
PackageManager.MATCH_SYSTEM_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
if (matches.size() == 1) {
return matches.get(0).getComponentInfo().getComponentName();
} else {
Log.w(TAG, "Didn't find exactly one BroadcastReceiver to handle "
+ Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED
+ ". The report will be denied. size="
+ matches.size() + ": matches=" + matches);
return null;
}
}
/**
* Return whether the package is one of the packages installed for the uid.
*/
private boolean isPackageInUid(int uid, String packageName) {
try {
mAppOpsManager.checkPackage(uid, packageName);
return true;
} catch (SecurityException ex) {
return false;
}
}
/**
* Checks if the 2 provided user ids belong to the same profile group
* using {@link UserManager#isSameProfileGroup(int, int)}
*/
private boolean isSameProfileGroupUser(@UserIdInt int currentAdminUser,
@UserIdInt int callingUser) {
return UserManager.get(mContext)
.isSameProfileGroup(currentAdminUser, callingUser);
}
}