/*
 * Copyright (C) 2021 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.
 */
// @exportToGMSCore:skipFile()
package com.android.server.appsearch.visibilitystore;

import static android.Manifest.permission.EXECUTE_APP_FUNCTIONS;
import static android.Manifest.permission.PACKAGE_USAGE_STATS;
import static android.Manifest.permission.READ_ASSISTANT_APP_SEARCH_DATA;
import static android.Manifest.permission.READ_CALENDAR;
import static android.Manifest.permission.READ_CONTACTS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_GLOBAL_APP_SEARCH_DATA;
import static android.Manifest.permission.READ_HOME_APP_SEARCH_DATA;
import static android.Manifest.permission.READ_SMS;
import static android.permission.PermissionManager.PERMISSION_GRANTED;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresApi;
import android.app.appfunctions.AppFunctionManager;
import android.app.appsearch.InternalVisibilityConfig;
import android.app.appsearch.PackageIdentifier;
import android.app.appsearch.SchemaVisibilityConfig;
import android.app.appsearch.SetSchemaRequest;
import android.app.appsearch.aidl.AppSearchAttributionSource;
import android.content.AttributionSource;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.UserHandle;
import android.permission.PermissionManager;

import com.android.appsearch.flags.Flags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.appsearch.external.localstorage.visibilitystore.CallerAccess;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityChecker;
import com.android.server.appsearch.external.localstorage.visibilitystore.VisibilityStore;
import com.android.server.appsearch.util.PackageManagerUtil;
import com.android.server.appsearch.util.PackageUtil;

import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * A platform implementation of {@link VisibilityChecker}.
 *
 * @hide
 */
public class VisibilityCheckerImpl implements VisibilityChecker {
    // Context of the user that the call is being made as.
    private final Context mUserContext;
    private final PermissionManager mPermissionManager;
    private final PolicyChecker mPolicyChecker;

    private final AppFunctionCompat mAppFunctionCompat;

    public VisibilityCheckerImpl(@NonNull Context userContext) {
        this(userContext, new PolicyCheckerImpl(userContext));
    }

    @VisibleForTesting
    VisibilityCheckerImpl(@NonNull Context userContext, @NonNull PolicyChecker policyChecker) {
        mUserContext = Objects.requireNonNull(userContext);
        mPermissionManager = userContext.getSystemService(PermissionManager.class);
        mPolicyChecker = policyChecker;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
            mAppFunctionCompat = new Api36Impl(userContext);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            mAppFunctionCompat = new Api33Impl();
        } else {
            mAppFunctionCompat = new NoOpApiImpl();
        }
    }

    @Override
    public boolean isSchemaSearchableByCaller(
            @NonNull CallerAccess callerAccess,
            @NonNull String packageName,
            @NonNull String prefixedSchema,
            @NonNull VisibilityStore visibilityStore) {
        Objects.requireNonNull(callerAccess);
        Objects.requireNonNull(packageName);
        Objects.requireNonNull(prefixedSchema);
        if (packageName.equals(VisibilityStore.VISIBILITY_PACKAGE_NAME)) {
            return false; // VisibilityStore schemas are for internal bookkeeping.
        }

        FrameworkCallerAccess frameworkCallerAccess = (FrameworkCallerAccess) callerAccess;
        InternalVisibilityConfig internalVisibilityConfig =
                visibilityStore.getVisibility(prefixedSchema);

        // If caller requires enterprise access, the given schema is only visible if there exists a
        // required permissions set containing ENTERPRISE_ACCESS that the caller passes; the passing
        // set may be part of a SchemaVisibilityConfig
        if (frameworkCallerAccess.isForEnterprise()) {
            if (internalVisibilityConfig == null) {
                return false;
            }
            if (isSchemaVisibleToPermission(
                    internalVisibilityConfig.getVisibilityConfig(),
                    frameworkCallerAccess.getCallingAttributionSource(),
                    /* checkEnterpriseAccess= */ true)) {
                return true;
            }
            if (Flags.enableEnterpriseVisibleToConfig()) {
                Set<SchemaVisibilityConfig> visibleToConfigs =
                        internalVisibilityConfig.getVisibleToConfigs();
                for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
                    if (checkMatchAllVisibilityConfig(frameworkCallerAccess,
                            visibleToConfig, /* checkEnterpriseAccess= */ true)) {
                        return true;
                    }
                }
            }
            return false;
        }

        if (internalVisibilityConfig == null) {
            // The target schema doesn't exist yet. We will treat it as default setting and the only
            // accessible case is when the caller has system access.
            return frameworkCallerAccess.doesCallerHaveSystemAccess();
        }

        // Check whether the calling package has system access and the target schema is visible to
        // the system.
        if (frameworkCallerAccess.doesCallerHaveSystemAccess()
                && !internalVisibilityConfig.isNotDisplayedBySystem()) {
            return true;
        }

        // Check OR visibility settings. Caller could access if they match ANY of the requirements
        // in the visibilityConfig.
        SchemaVisibilityConfig visibilityConfig = internalVisibilityConfig.getVisibilityConfig();
        if (checkMatchAnyVisibilityConfig(frameworkCallerAccess, visibilityConfig)) {
            return true;
        }

        // Check AND visibility settings. Caller could access if they match ALL of the requirements
        // in the visibilityConfig.
        Set<SchemaVisibilityConfig> visibleToConfigs =
                internalVisibilityConfig.getVisibleToConfigs();
        for (SchemaVisibilityConfig visibleToConfig : visibleToConfigs) {
            if (checkMatchAllVisibilityConfig(frameworkCallerAccess,
                    visibleToConfig, /* checkEnterpriseAccess= */ false)) {
                return true;
            }
        }
        return false;
    }

    /** Check whether the caller math ANY of the visibility requirements. */
    private boolean checkMatchAnyVisibilityConfig(
            @NonNull FrameworkCallerAccess frameworkCallerAccess,
            @NonNull SchemaVisibilityConfig visibilityConfig) {
        if (isSchemaVisibleToPackages(
                visibilityConfig, frameworkCallerAccess.getCallingAttributionSource().getUid())) {
            // The caller is in the allow list and has access to the given schema.
            return true;
        }

        // Check whether caller has all required permissions for the given schema.
        if (isSchemaVisibleToPermission(
                visibilityConfig,
                frameworkCallerAccess.getCallingAttributionSource(),
                /* checkEnterpriseAccess= */ false)) {
            return true;
        }

        // Check whether the calling package has visibility to the package providing the schema.
        return isSchemaPubliclyVisibleFromPackage(visibilityConfig, frameworkCallerAccess);
    }

    /** Check whether the caller math ALL of the visibility requirements. */
    private boolean checkMatchAllVisibilityConfig(
            @NonNull FrameworkCallerAccess frameworkCallerAccess,
            @NonNull SchemaVisibilityConfig visibilityConfig,
            boolean checkEnterpriseAccess) {

        // We will skip following checks if user never specific them. But the caller should has
        // passed at least one check to get the access.
        boolean hasPassedCheck = false;
        boolean hasPassedRequiredPermissionsCheck = false;

        // Check whether the caller is in the allow list and has access to the given schema.
        if (!visibilityConfig.getAllowedPackages().isEmpty()) {
            if (!isSchemaVisibleToPackages(
                    visibilityConfig,
                    frameworkCallerAccess.getCallingAttributionSource().getUid())) {
                return false; // Return early for the 'ALL' case.
            }
            hasPassedCheck = true;
        }

        // Check whether caller has all required permissions for the given schema.
        if (!visibilityConfig.getRequiredPermissions().isEmpty()) {
            if (!isSchemaVisibleToPermission(
                    visibilityConfig,
                    frameworkCallerAccess.getCallingAttributionSource(),
                    checkEnterpriseAccess)) {
                return false; // Return early for the 'ALL' case.
            }
            hasPassedCheck = true;
            hasPassedRequiredPermissionsCheck = true;
        }

        // Check whether the calling package has visibility to the package providing the schema.
        if (visibilityConfig.getPubliclyVisibleTargetPackage() != null) {
            if (!isSchemaPubliclyVisibleFromPackage(visibilityConfig, frameworkCallerAccess)) {
                return false; // Return early for the 'ALL' case.
            }
            hasPassedCheck = true;
        }

        // If enterprise call, must also pass a required permissions check
        return hasPassedCheck && (!checkEnterpriseAccess || hasPassedRequiredPermissionsCheck);
    }

    private boolean isSchemaPubliclyVisibleFromPackage(
            @NonNull SchemaVisibilityConfig visibilityConfig,
            FrameworkCallerAccess frameworkCallerAccess) {
        PackageIdentifier targetPackage = visibilityConfig.getPubliclyVisibleTargetPackage();
        if (targetPackage == null) {
            return false;
        }

        // Ensure the sha 256 certificate matches the certificate of the actual publicly visible
        // target package.
        if (!PackageManagerUtil.hasSigningCertificate(
                mUserContext,
                targetPackage.getPackageName(),
                targetPackage.getSha256Certificate())) {
            return false;
        }

        // We cannot use the package name of the schema itself because the schema could be in a
        // separate package from the publicly visible target package. For instance, Apps Indexer
        // could store a document representing a timer app in a schema in the android package, but
        // the publicly visible target package for that schema could be the timer app package.
        try {
            // The call that opens up documents to "public" access
            if (mUserContext
                    .getPackageManager()
                    .canPackageQuery(
                            frameworkCallerAccess.getCallingPackageName(),
                            targetPackage.getPackageName())) {
                return true;
            }
        } catch (PackageManager.NameNotFoundException e) {
            // One or both of the packages doesn't exist. Either way, we don't have public
            // visibility to the target package, so continue to return false.
        }
        return false;
    }

    /**
     * Returns whether the schema is accessible by the {@code callerUid}. Checks that the callerUid
     * has one of the allowed PackageIdentifier's package. And if so, that the package also has the
     * matching certificate.
     *
     * <p>This supports packages that have certificate rotation. As long as the specified
     * certificate was once used to sign the package, the package will still be granted access. This
     * does not handle packages that have been signed by multiple certificates.
     */
    private boolean isSchemaVisibleToPackages(
            @NonNull SchemaVisibilityConfig visibilityConfig, int callerUid) {
        List<PackageIdentifier> visibleToPackages = visibilityConfig.getAllowedPackages();
        for (int i = 0; i < visibleToPackages.size(); i++) {
            PackageIdentifier visibleToPackage = visibleToPackages.get(i);

            // TODO(b/169883602): Consider caching the UIDs of packages. Looking this up in the
            // package manager could be costly. We would also need to update the cache on
            // package-removals.

            // 'callerUid' is the uid of the caller. The 'user' doesn't have to be the same one as
            // the callerUid since clients can createContextAsUser with some other user, and then
            // make calls to us. So just check if the appId portion of the uid is the same. This is
            // essentially UserHandle.isSameApp, but that's not a system API for us to use.
            int callerAppId = UserHandle.getAppId(callerUid);
            int packageUid =
                    PackageUtil.getPackageUid(mUserContext, visibleToPackage.getPackageName());
            int userAppId = UserHandle.getAppId(packageUid);
            if (callerAppId != userAppId) {
                continue;
            }

            // Check that the package also has the matching certificate
            if (PackageManagerUtil.hasSigningCertificate(
                    mUserContext,
                    visibleToPackage.getPackageName(),
                    visibleToPackage.getSha256Certificate())) {
                // The caller has the right package name and right certificate!
                return true;
            }
        }
        // If we can't verify the schema is package accessible, default to no access.
        return false;
    }

    /** Returns whether the caller holds required permissions for the given schema. */
    private boolean isSchemaVisibleToPermission(
            @NonNull SchemaVisibilityConfig visibilityConfig,
            @Nullable AppSearchAttributionSource callerAttributionSource,
            boolean checkEnterpriseAccess) {
        Set<Set<Integer>> visibleToPermissions = visibilityConfig.getRequiredPermissions();
        if (visibleToPermissions.isEmpty() || callerAttributionSource == null) {
            // Provider doesn't set any permissions or there is no caller attribution source,
            // default is not accessible to anyone.
            return false;
        }
        for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
            // User may set multiple required permission sets. Provider need to hold ALL required
            // permission of ANY of the individual value sets.
            if (doesCallerHoldsAllRequiredPermissions(
                    allRequiredPermissions, callerAttributionSource, checkEnterpriseAccess)) {
                // The calling package has all required permissions in this set, return true.
                return true;
            }
        }
        // The calling doesn't hold all required permissions for any individual sets, return false.
        return false;
    }

    /** Returns true if the caller holds all required permissions in the given set. */
    private boolean doesCallerHoldsAllRequiredPermissions(
            @NonNull Set<Integer> allRequiredPermissions,
            @NonNull AppSearchAttributionSource callerAttributionSource,
            boolean checkEnterpriseAccess) {
        // A permissions set with ENTERPRISE_ACCESS should only be checked by enterprise calls,
        // and enterprise calls should only check permission sets with ENTERPRISE_ACCESS
        boolean isEnterprisePermissionsSet =
                allRequiredPermissions.contains(SetSchemaRequest.ENTERPRISE_ACCESS);
        if (checkEnterpriseAccess != isEnterprisePermissionsSet) {
            return false;
        }
        String callingPackageName;
        for (int requiredPermission : allRequiredPermissions) {
            switch (requiredPermission) {
                case SetSchemaRequest.READ_SMS:
                case SetSchemaRequest.READ_CALENDAR:
                case SetSchemaRequest.READ_CONTACTS:
                case SetSchemaRequest.READ_EXTERNAL_STORAGE:
                case SetSchemaRequest.READ_HOME_APP_SEARCH_DATA:
                case SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA:
                case SetSchemaRequest.PACKAGE_USAGE_STATS:
                    if (!doesCallerHavePermissionForDataDelivery(
                            requiredPermission, callerAttributionSource)) {
                        // The calling package doesn't have this required permission, return false.
                        return false;
                    }
                    break;
                case SetSchemaRequest.EXECUTE_APP_FUNCTIONS:
                case SetSchemaRequest.EXECUTE_APP_FUNCTIONS_TRUSTED:
                    if (!doesCallerHavePermissionForDataDelivery(
                            requiredPermission, callerAttributionSource)) {
                        // The calling package doesn't have this required permission, return false.
                        return false;
                    }
                    if (Flags.enableAppFunctionAgentAllowlistCheck()) {
                        if (!isValidAppFunctionAgent(callerAttributionSource.getPackageName())) {
                            return false;
                        }
                    }
                    break;
                case SetSchemaRequest.MANAGED_PROFILE_CONTACTS_ACCESS:
                    // The managed profile access check should only be done from enterprise
                    if (!checkEnterpriseAccess) {
                        return false;
                    }
                    callingPackageName = callerAttributionSource.getPackageName();
                    if (callingPackageName == null
                            || !mPolicyChecker.doesCallerHaveManagedProfileContactsAccess(
                                    callingPackageName)) {
                        return false;
                    }
                    break;
                case SetSchemaRequest.ENTERPRISE_ACCESS:
                    // This permission is not an actual permission; it marks the permission set as
                    // enterprise and is checked at the top of this method so just skip it here
                    break;
                default:
                    throw new UnsupportedOperationException(
                            "The required permission is unsupported in AppSearch : "
                                    + requiredPermission);
            }
        }
        // The calling package has all required permissions in this set, return true.
        return true;
    }

    /**
     * Checks whether the calling package has the corresponding Android permission to the specified
     * {@code requiredPermission}.
     */
    // Suppressing warning about not guarding by SDK level check since this method is manually
    // tested to work on older devices that don't have permissions declared in higher SDKs
    // (returning false by default if permission does not exist).
    @SuppressWarnings("InlinedApi")
    private boolean doesCallerHavePermissionForDataDelivery(
            @SetSchemaRequest.AppSearchSupportedPermission int requiredPermission,
            @NonNull AppSearchAttributionSource callerAttributionSource) {
        String permission;
        switch (requiredPermission) {
            case SetSchemaRequest.READ_SMS:
                permission = READ_SMS;
                break;
            case SetSchemaRequest.READ_CALENDAR:
                permission = READ_CALENDAR;
                break;
            case SetSchemaRequest.READ_CONTACTS:
                permission = READ_CONTACTS;
                break;
            case SetSchemaRequest.READ_EXTERNAL_STORAGE:
                permission = READ_EXTERNAL_STORAGE;
                break;
            case SetSchemaRequest.READ_HOME_APP_SEARCH_DATA:
                permission = READ_HOME_APP_SEARCH_DATA;
                break;
            case SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA:
                permission = READ_ASSISTANT_APP_SEARCH_DATA;
                break;
            case SetSchemaRequest.EXECUTE_APP_FUNCTIONS:
                permission = EXECUTE_APP_FUNCTIONS;
                break;
            case SetSchemaRequest.PACKAGE_USAGE_STATS:
                permission = PACKAGE_USAGE_STATS;
                break;
            case SetSchemaRequest.EXECUTE_APP_FUNCTIONS_TRUSTED:
                // Deprecated. Returning false as is this permission does not exist.
                return false;
            default:
                return false;
        }
        // getAttributionSource can be safely called and the returned value will only be
        // null on Android R-
        return checkPermissionForDataDeliveryGranted(
                permission,
                callerAttributionSource.getAttributionSource(),
                /* message= */ "appsearch");
    }

    /**
     * Checks whether permission for data delivery with {@link PermissionManager} is granted.
     *
     * @hide
     */
    @VisibleForTesting
    public boolean checkPermissionForDataDeliveryGranted(
            @NonNull String permission,
            @NonNull AttributionSource attributionSource,
            @Nullable String message) {
        return PERMISSION_GRANTED
                == mPermissionManager.checkPermissionForDataDelivery(
                        permission, attributionSource, message);
    }

    /**
     * Checks whether the given package has access to system-surfaceable schemas.
     *
     * @param callerPackageName Package name of the caller.
     */
    @Override
    public boolean doesCallerHaveSystemAccess(@NonNull String callerPackageName) {
        Objects.requireNonNull(callerPackageName);
        return mUserContext
                        .getPackageManager()
                        .checkPermission(READ_GLOBAL_APP_SEARCH_DATA, callerPackageName)
                == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * Checks whether the given package is a valid agent.
     *
     * @param callerPackageName Package name of the caller.
     */
    @VisibleForTesting
    public boolean isValidAppFunctionAgent(@NonNull String callerPackageName) {
        return mAppFunctionCompat.isValidAppFunctionAgent(callerPackageName);
    }

    private interface AppFunctionCompat {
        /**
         * Checks whether the given package is a valid agent.
         *
         * @param callerPackageName Package name of the caller.
         */
        boolean isValidAppFunctionAgent(@NonNull String callerPackageName);
    }

    private static class NoOpApiImpl implements AppFunctionCompat {
        /**
         * For SDK < 33, backporting AppFunction feature is not supported. All agents should be
         * considered as invalid.
         */
        @Override
        public boolean isValidAppFunctionAgent(@NonNull String callerPackageName) {
            return false;
        }
    }

    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private static class Api33Impl implements AppFunctionCompat {
        /**
         * For SDK between 35 and 33, backporting AppFunction feature is supported and there is no
         * concept of valid agent yet. Therefore, all agent are considered as valid.
         */
        @Override
        public boolean isValidAppFunctionAgent(@NonNull String callerPackageName) {
            return true;
        }
    }

    @RequiresApi(Build.VERSION_CODES.BAKLAVA)
    private static class Api36Impl implements AppFunctionCompat {
        @NonNull private final AppFunctionManager mAppFunctionManager;

        Api36Impl(@NonNull Context context) {
            Objects.requireNonNull(context);
            mAppFunctionManager =
                    Objects.requireNonNull(context.getSystemService(AppFunctionManager.class));
        }

        /**
         * For SDK >= 36, if the access API is enabled, only the agents in {@link
         * AppFunctionManager#getValidAgents()} are considered as a valid agent.
         */
        @Override
        public boolean isValidAppFunctionAgent(@NonNull String callerPackageName) {
            if (!android.permission.flags.Flags.appFunctionAccessApiEnabled()) {
                // Running on SDK without AppFunction access service, any app with
                // EXECUTE_APP_FUNCTIONS permission is a valid agent.
                return true;
            }

            Objects.requireNonNull(callerPackageName);
            final List<String> validAgents = mAppFunctionManager.getValidAgents();
            return validAgents.contains(callerPackageName);
        }
    }
}
