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

Commit 6adf3761 authored by Azhara Assanova's avatar Azhara Assanova
Browse files

Add ComponentCaller#checkContentUriPermission API for Activity

The new API checks if the app that launched the activity, i.e. activity
caller, had access to the content URI at launch time. It doesn't perform
a real time check to ensure no loss of grant information in the case of
caller's termination, hence only works for content URIs passed at
launch to mimic the lifetime of grant flags. Finally, for security
reasons, the method requires the caller of the API to the same access to
the content URI, otherwise it throws.

The code for this check is located in a new class ActivityCallerState.

Bug: 293467489
Test: atest CtsAndroidAppTestCases:android.app.cts.ComponentCallerTest
Change-Id: Ide1ea3470e8cc48f4d59e431ef19681050273af2
parent f12aa38b
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -5425,6 +5425,7 @@ package android.app {
  @FlaggedApi("android.security.content_uri_permission_apis") public final class ComponentCaller {
    ctor public ComponentCaller(@NonNull android.os.IBinder, @Nullable android.os.IBinder);
    method public int checkContentUriPermission(@NonNull android.net.Uri, int);
    method @Nullable public String getPackage();
    method public int getUid();
  }
+15 −0
Original line number Diff line number Diff line
@@ -17,13 +17,16 @@
package android.app;

import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
import static android.os.UserHandle.getCallingUserId;

import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
@@ -296,6 +299,18 @@ public class ActivityClient {
        }
    }

    /** Checks if the app that launched the activity has access to the URI. */
    public int checkActivityCallerContentUriPermission(IBinder activityToken, IBinder callerToken,
            Uri uri, int modeFlags) {
        try {
            return getActivityClientController().checkActivityCallerContentUriPermission(
                    activityToken, callerToken, ContentProvider.getUriWithoutUserId(uri), modeFlags,
                    ContentProvider.getUserIdFromUri(uri, getCallingUserId()));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    public void setRequestedOrientation(IBinder token, int requestedOrientation) {
        try {
            getActivityClientController().setRequestedOrientation(token, requestedOrientation);
+37 −0
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package android.app;

import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.IBinder;
import android.os.Process;

@@ -118,6 +121,40 @@ public final class ComponentCaller {
        return ActivityClient.getInstance().getLaunchedFromPackage(mActivityToken);
    }

    /**
     * Determines whether this component caller had access to a specific content URI at launch time.
     * Apps can use this API to validate content URIs coming from other apps.
     *
     * <p><b>Note</b>, in {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} only
     * {@link Activity} has access to {@link ComponentCaller} instances.
     *
     * <p>Before using this method, note the following:
     * <ul>
     *     <li>You must have access to the supplied URI, otherwise it will throw a
     *     {@link SecurityException}.
     *     <li>This is not a real time check, i.e. the permissions have been computed at launch
     *     time.
     *     <li>This method will return the correct result for content URIs passed at launch time,
     *     specifically the ones from {@link Intent#getData()}, and {@link Intent#getClipData()} in
     *     the intent of {@code startActivity(intent)}. For others, it will throw an
     *     {@link IllegalArgumentException}.
     * </ul>
     *
     * @param uri The content uri that is being checked
     * @param modeFlags The access modes to check
     * @return {@link PackageManager#PERMISSION_GRANTED} if this activity caller is allowed to
     *         access that uri, or {@link PackageManager#PERMISSION_DENIED} if it is not
     * @throws IllegalArgumentException if uri is a non-content URI or it wasn't passed at launch
     * @throws SecurityException if you don't have access to uri
     *
     * @see android.content.Context#checkContentUriPermissionFull(Uri, int, int, int)
     */
    @PackageManager.PermissionResult
    public int checkContentUriPermission(@NonNull Uri uri, @Intent.AccessUriMode int modeFlags) {
        return ActivityClient.getInstance().checkActivityCallerContentUriPermission(mActivityToken,
                mCallerToken, uri, modeFlags);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj == null || !(obj instanceof ComponentCaller other)) {
+4 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.app.PictureInPictureParams;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.PersistableBundle;
@@ -91,6 +92,9 @@ interface IActivityClientController {
    int getLaunchedFromUid(in IBinder token);
    String getLaunchedFromPackage(in IBinder token);

    int checkActivityCallerContentUriPermission(in IBinder activityToken, in IBinder callerToken,
            in Uri uri, int modeFlags, int userId);

    void setRequestedOrientation(in IBinder token, int requestedOrientation);
    int getRequestedOrientation(in IBinder token);

+245 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.wm;

import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;

import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

import android.content.ClipData;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;

import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.uri.GrantUri;

import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.WeakHashMap;

/**
 * Represents the state of activity callers. Used by {@link ActivityRecord}.
 * @hide
 */
final class ActivityCallerState {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityCallerState" : TAG_ATM;

    // XML tags for CallerInfo
    private static final String TAG_READABLE_CONTENT_URI = "readable_content_uri";
    private static final String TAG_WRITABLE_CONTENT_URI = "writable_content_uri";
    private static final String TAG_INACCESSIBLE_CONTENT_URI = "inaccessible_content_uri";
    private static final String ATTR_SOURCE_USER_ID = "source_user_id";
    private static final String ATTR_URI = "uri";
    private static final String ATTR_PREFIX = "prefix";

    // Map for storing CallerInfo instances
    private final WeakHashMap<IBinder, CallerInfo> mCallerTokenInfoMap = new WeakHashMap<>();

    final ActivityTaskManagerService mAtmService;

    ActivityCallerState(ActivityTaskManagerService service) {
        mAtmService = service;
    }

    CallerInfo getCallerInfoOrNull(IBinder callerToken) {
        return mCallerTokenInfoMap.getOrDefault(callerToken, null);
    }

    void add(IBinder callerToken, CallerInfo callerInfo) {
        mCallerTokenInfoMap.put(callerToken, callerInfo);
    }

    void computeCallerInfo(IBinder callerToken, Intent intent, int callerUid) {
        final CallerInfo callerInfo = new CallerInfo();
        mCallerTokenInfoMap.put(callerToken, callerInfo);

        final ArraySet<Uri> contentUris = getContentUrisFromIntent(intent);
        for (int i = contentUris.size() - 1; i >= 0; i--) {
            final Uri contentUri = contentUris.valueAt(i);

            final boolean hasRead = addContentUriIfUidHasPermission(contentUri, callerUid,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION, callerInfo.mReadableContentUris);

            final boolean hasWrite = addContentUriIfUidHasPermission(contentUri, callerUid,
                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION, callerInfo.mWritableContentUris);

            if (!hasRead && !hasWrite) {
                callerInfo.mInaccessibleContentUris.add(convertToGrantUri(contentUri,
                        /* modeFlags */ 0));
            }
        }
    }

    boolean checkContentUriPermission(IBinder callerToken, GrantUri grantUri, int modeFlags) {
        if (!Intent.isAccessUriMode(modeFlags)) {
            throw new IllegalArgumentException("Mode flags are not access URI mode flags: "
                    + modeFlags);
        }

        final CallerInfo callerInfo = mCallerTokenInfoMap.getOrDefault(callerToken, null);
        if (callerInfo == null) {
            Slog.e(TAG, "Caller not found for checkContentUriPermission of: "
                    + grantUri.uri.toSafeString());
            return false;
        }

        if (callerInfo.mInaccessibleContentUris.contains(grantUri)) {
            return false;
        }

        final boolean readMet = callerInfo.mReadableContentUris.contains(grantUri);
        final boolean writeMet = callerInfo.mWritableContentUris.contains(grantUri);

        if (!readMet && !writeMet) {
            throw new IllegalArgumentException("The supplied URI wasn't passed at launch: "
                    + grantUri.uri.toSafeString());
        }

        final boolean checkRead = (modeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0;
        if (checkRead && !readMet) {
            return false;
        }

        final boolean checkWrite = (modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0;
        if (checkWrite && !writeMet) {
            return false;
        }

        return true;
    }

    private boolean addContentUriIfUidHasPermission(Uri contentUri, int uid, int modeFlags,
            ArraySet<GrantUri> grantUris) {
        final GrantUri grantUri = convertToGrantUri(contentUri, modeFlags);
        if (mAtmService.mUgmInternal.checkUriPermission(grantUri, uid,
                modeFlags, /* isFullAccessForContentUri */ true)) {
            grantUris.add(grantUri);
            return true;
        }
        return false;
    }

    private static GrantUri convertToGrantUri(Uri contentUri, int modeFlags) {
        return new GrantUri(ContentProvider.getUserIdFromUri(contentUri,
                UserHandle.getCallingUserId()), ContentProvider.getUriWithoutUserId(contentUri),
                modeFlags);
    }

    private static ArraySet<Uri> getContentUrisFromIntent(Intent intent) {
        final ArraySet<Uri> uris = new ArraySet<>();
        if (intent == null) return uris;

        // getData
        addUriIfContentUri(intent.getData(), uris);

        final ClipData clipData = intent.getClipData();
        if (clipData == null) return uris;

        for (int i = 0; i < clipData.getItemCount(); i++) {
            final ClipData.Item item = clipData.getItemAt(i);

            // getUri
            addUriIfContentUri(item.getUri(), uris);

            // getIntent
            uris.addAll(getContentUrisFromIntent(item.getIntent()));
        }
        return uris;
    }

    private static void addUriIfContentUri(Uri uri, ArraySet<Uri> uris) {
        if (uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
            uris.add(uri);
        }
    }

    public static final class CallerInfo {
        final ArraySet<GrantUri> mReadableContentUris = new ArraySet<>();
        final ArraySet<GrantUri> mWritableContentUris = new ArraySet<>();
        final ArraySet<GrantUri> mInaccessibleContentUris = new ArraySet<>();

        public void saveToXml(TypedXmlSerializer out)
                throws IOException, XmlPullParserException {
            for (int i = mReadableContentUris.size() - 1; i >= 0; i--) {
                saveGrantUriToXml(out, mReadableContentUris.valueAt(i), TAG_READABLE_CONTENT_URI);
            }

            for (int i = mWritableContentUris.size() - 1; i >= 0; i--) {
                saveGrantUriToXml(out, mWritableContentUris.valueAt(i), TAG_WRITABLE_CONTENT_URI);
            }

            for (int i = mInaccessibleContentUris.size() - 1; i >= 0; i--) {
                saveGrantUriToXml(out, mInaccessibleContentUris.valueAt(i),
                        TAG_INACCESSIBLE_CONTENT_URI);
            }
        }

        public static CallerInfo restoreFromXml(TypedXmlPullParser in)
                throws IOException, XmlPullParserException {
            CallerInfo callerInfo = new CallerInfo();
            final int outerDepth = in.getDepth();
            int event;
            while (((event = in.next()) != END_DOCUMENT)
                    && (event != END_TAG || in.getDepth() >= outerDepth)) {
                if (event == START_TAG) {
                    final String name = in.getName();
                    if (TAG_READABLE_CONTENT_URI.equals(name)) {
                        callerInfo.mReadableContentUris.add(restoreGrantUriFromXml(in));
                    } else if (TAG_WRITABLE_CONTENT_URI.equals(name)) {
                        callerInfo.mWritableContentUris.add(restoreGrantUriFromXml(in));
                    } else if (TAG_INACCESSIBLE_CONTENT_URI.equals(name)) {
                        callerInfo.mInaccessibleContentUris.add(restoreGrantUriFromXml(in));
                    } else {
                        Slog.w(TAG, "restoreActivity: unexpected name=" + name);
                        XmlUtils.skipCurrentTag(in);
                    }
                }
            }
            return callerInfo;
        }

        private void saveGrantUriToXml(TypedXmlSerializer out, GrantUri grantUri, String tag)
                throws IOException, XmlPullParserException {
            out.startTag(null, tag);
            out.attributeInt(null, ATTR_SOURCE_USER_ID, grantUri.sourceUserId);
            out.attribute(null, ATTR_URI, String.valueOf(grantUri.uri));
            out.attributeBoolean(null, ATTR_PREFIX, grantUri.prefix);
            out.endTag(null, tag);
        }

        private static GrantUri restoreGrantUriFromXml(TypedXmlPullParser in)
                throws IOException, XmlPullParserException {
            int sourceUserId = in.getAttributeInt(null, ATTR_SOURCE_USER_ID, 0);
            Uri uri = Uri.parse(in.getAttributeValue(null, ATTR_URI));
            boolean prefix = in.getAttributeBoolean(null, ATTR_PREFIX, false);
            return new GrantUri(sourceUserId, uri,
                    prefix ? Intent.FLAG_GRANT_PREFIX_URI_PERMISSION : 0);
        }
    }
}
Loading