/*
 * Copyright 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.
 */

package com.android.net.module.util;

import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
import static android.Manifest.permission.NETWORK_STACK;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;

import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.UserHandle;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Collection of permission utilities.
 * @hide
 */
public final class PermissionUtils {
    /**
     * Return true if the context has one of given permission.
     */
    @CheckResult
    public static boolean hasAnyPermissionOf(@NonNull Context context,
                                             @NonNull String... permissions) {
        for (String permission : permissions) {
            if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return true if the context has one of given permission that is allowed
     * for a particular process and user ID running in the system.
     */
    @CheckResult
    public static boolean hasAnyPermissionOf(@NonNull Context context,
                                             int pid, int uid, @NonNull String... permissions) {
        for (String permission : permissions) {
            if (context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED) {
                return true;
            }
        }
        return false;
    }

    /**
     * Enforce permission check on the context that should have one of given permission.
     */
    public static void enforceAnyPermissionOf(@NonNull Context context,
            @NonNull String... permissions) {
        if (!hasAnyPermissionOf(context, permissions)) {
            throw new SecurityException("Requires one of the following permissions: "
                    + String.join(", ", permissions) + ".");
        }
    }

    /**
     * If the NetworkStack, MAINLINE_NETWORK_STACK are not allowed for a particular process, throw a
     * {@link SecurityException}.
     *
     * @param context {@link android.content.Context} for the process.
     */
    public static void enforceNetworkStackPermission(final @NonNull Context context) {
        enforceNetworkStackPermissionOr(context);
    }

    /**
     * If the NetworkStack, MAINLINE_NETWORK_STACK or other specified permissions are not allowed
     * for a particular process, throw a {@link SecurityException}.
     *
     * @param context {@link android.content.Context} for the process.
     * @param otherPermissions The set of permissions that could be the candidate permissions , or
     *                         empty string if none of other permissions needed.
     */
    public static void enforceNetworkStackPermissionOr(final @NonNull Context context,
            final @NonNull String... otherPermissions) {
        ArrayList<String> permissions = new ArrayList<String>(Arrays.asList(otherPermissions));
        permissions.add(NETWORK_STACK);
        permissions.add(PERMISSION_MAINLINE_NETWORK_STACK);
        enforceAnyPermissionOf(context, permissions.toArray(new String[0]));
    }

    /**
     * If the CONNECTIVITY_USE_RESTRICTED_NETWORKS is not allowed for a particular process, throw a
     * {@link SecurityException}.
     *
     * @param context {@link android.content.Context} for the process.
     * @param message A message to include in the exception if it is thrown.
     */
    public static void enforceRestrictedNetworkPermission(
            final @NonNull Context context, final @Nullable String message) {
        context.enforceCallingOrSelfPermission(CONNECTIVITY_USE_RESTRICTED_NETWORKS, message);
    }

    /**
     * If the ACCESS_NETWORK_STATE is not allowed for a particular process, throw a
     * {@link SecurityException}.
     *
     * @param context {@link android.content.Context} for the process.
     * @param message A message to include in the exception if it is thrown.
     */
    public static void enforceAccessNetworkStatePermission(
            final @NonNull Context context, final @Nullable String message) {
        context.enforceCallingOrSelfPermission(ACCESS_NETWORK_STATE, message);
    }

    /**
     * Return true if the context has DUMP permission.
     */
    @CheckResult
    public static boolean hasDumpPermission(Context context, String tag, PrintWriter pw) {
        if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
                != PERMISSION_GRANTED) {
            pw.println("Permission Denial: can't dump " + tag + " from from pid="
                    + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
                    + " due to missing android.permission.DUMP permission");
            return false;
        } else {
            return true;
        }
    }

    /**
     * Enforce that a given feature is available and if not, throw an
     * {@link UnsupportedOperationException}.
     *
     * @param context {@link android.content.Context} for the process.
     * @param feature the feature name to enforce.
     * @param errorMessage an optional error message to include.
     */
    public static void enforceSystemFeature(final @NonNull Context context,
            final @NonNull String feature, final @Nullable String errorMessage) {
        final boolean hasSystemFeature =
                context.getPackageManager().hasSystemFeature(feature);
        if (!hasSystemFeature) {
            if (null == errorMessage) {
                throw new UnsupportedOperationException();
            }
            throw new UnsupportedOperationException(errorMessage);
        }
    }

    /**
     * Get the list of granted permissions for a package info.
     *
     * PackageInfo contains the list of requested permissions, and their state (whether they
     * were granted or not, in particular) as a parallel array. Most users care only about
     * granted permissions. This method returns the list of them.
     *
     * @param packageInfo the package info for the relevant uid.
     * @return the list of granted permissions.
     */
    public static List<String> getGrantedPermissions(final @NonNull PackageInfo packageInfo) {
        if (null == packageInfo.requestedPermissions) return Collections.emptyList();
        final ArrayList<String> result = new ArrayList<>(packageInfo.requestedPermissions.length);
        for (int i = 0; i < packageInfo.requestedPermissions.length; ++i) {
            if (0 != (REQUESTED_PERMISSION_GRANTED & packageInfo.requestedPermissionsFlags[i])) {
                result.add(packageInfo.requestedPermissions[i]);
            }
        }
        return result;
    }

    /**
     * Enforces that the given package name belongs to the given uid.
     * Note: b/377758490 - Figure out how to correct this to avoid mis-usage.
     * Meanwhile, avoid calling this method from the networkstack.
     *
     * @param context {@link android.content.Context} for the process.
     * @param uid User ID to check the package ownership for.
     * @param packageName Package name to verify.
     * @throws SecurityException If the package does not belong to the specified uid.
     */
    public static void enforcePackageNameMatchesUid(
            @NonNull Context context, int uid, @Nullable String packageName) {
        final UserHandle user = UserHandle.getUserHandleForUid(uid);
        if (getAppUid(context, packageName, user) != uid) {
            throw new SecurityException(packageName + " does not belong to uid " + uid);
        }
    }

    private static int getAppUid(Context context, final String app, final UserHandle user) {
        final PackageManager pm =
                context.createContextAsUser(user, 0 /* flags */).getPackageManager();
        final long token = Binder.clearCallingIdentity();
        try {
            return pm.getPackageUid(app, 0 /* flags */);
        } catch (PackageManager.NameNotFoundException e) {
            return -1;
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}
