/*
 * Copyright (C) 2022 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 android.os;

import android.annotation.NonNull;
import android.annotation.SystemService;
import android.app.AppOpsManager;
import android.content.AttributionSource;
import android.content.Context;
import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.permission.PermissionCheckerManager;
import android.permission.PermissionManager;

/**
 * PermissionEnforcer check permissions for AIDL-generated services which use
 * the @EnforcePermission annotation.
 *
 * <p>AIDL services may be annotated with @EnforcePermission which will trigger
 * the generation of permission check code. This generated code relies on
 * PermissionEnforcer to validate the permissions. The methods available are
 * purposely similar to the AIDL annotation syntax.
 *
 * <p>The constructor of the Stub generated by AIDL expects a
 * PermissionEnforcer. It can be based on the current Context. For example:
 *
 * <pre>{@code
 * class MyFoo extends Foo.Stub {
 *     MyFoo(Context context) {
 *         super(PermissionEnforcer.fromContext(context));
 *     }
 *
 *     @Override
 *     @EnforcePermission(android.Manifest.permission.INTERNET)
 *     public MyMethod() {
 *         MyMethod_enforcePermission();
 *     }
 * }
 * }</pre>
 *
 * <p>A {@link android.os.test.FakePermissionEnforcer} is available for unit
 * testing. It can be attached to a mocked Context using:
 * <pre>{@code
 * @Mock private Context mContext;
 *
 * @Before
 * public setUp() {
 *   fakeEnforcer = new FakePermissionEnforcer();
 *   fakeEnforcer.grant(android.Manifest.permission.INTERNET);
 *
 *   doReturn(fakeEnforcer).when(mContext).getSystemService(
                eq(Context.PERMISSION_ENFORCER_SERVICE));
 * }
 * }</pre>
 *
 * @see android.permission.PermissionManager
 *
 * @hide
 */
@SystemService(Context.PERMISSION_ENFORCER_SERVICE)
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class PermissionEnforcer {

    private final Context mContext;
    private static final String ACCESS_DENIED = "Access denied, requires: ";

    /** Protected constructor. Allows subclasses to instantiate an object
     *  without using a Context.
     */
    protected PermissionEnforcer() {
        mContext = null;
    }

    /** Constructor, prefer using the fromContext static method when possible */
    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = PermissionManager.class,
            reason = "Use subclass for unit tests, such as FakePermissionEnforcer")
    public PermissionEnforcer(@NonNull Context context) {
        mContext = context;
    }

    @PermissionCheckerManager.PermissionResult
    protected int checkPermission(@NonNull String permission, @NonNull AttributionSource source) {
        return PermissionChecker.checkPermissionForDataDelivery(
            mContext, permission, PermissionChecker.PID_UNKNOWN, source, "" /* message */);
    }

    @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck")
    @PermissionCheckerManager.PermissionResult
    protected int checkPermission(@NonNull String permission, int pid, int uid) {
        if (mContext.checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) {
            return PermissionCheckerManager.PERMISSION_GRANTED;
        }
        return PermissionCheckerManager.PERMISSION_HARD_DENIED;
    }

    @android.ravenwood.annotation.RavenwoodReplace(blockedBy = AppOpsManager.class,
            reason = "Blocked on Mainline dependencies")
    private static int permissionToOpCode(String permission) {
        return AppOpsManager.permissionToOpCode(permission);
    }

    private static int permissionToOpCode$ravenwood(String permission) {
        return AppOpsManager.OP_NONE;
    }

    private boolean anyAppOps(@NonNull String[] permissions) {
        for (String permission : permissions) {
            if (permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
                return true;
            }
        }
        return false;
    }

    public void enforcePermission(@NonNull String permission, @NonNull
            AttributionSource source) throws SecurityException {
        int result = checkPermission(permission, source);
        if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
            throw new SecurityException(ACCESS_DENIED + permission);
        }
    }

    public void enforcePermission(@NonNull String permission, int pid, int uid)
            throws SecurityException {
        if (permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
            AttributionSource source = new AttributionSource(uid, null, null);
            enforcePermission(permission, source);
            return;
        }
        int result = checkPermission(permission, pid, uid);
        if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
            throw new SecurityException(ACCESS_DENIED + permission);
        }
    }

    public void enforcePermissionAllOf(@NonNull String[] permissions,
            @NonNull AttributionSource source) throws SecurityException {
        for (String permission : permissions) {
            int result = checkPermission(permission, source);
            if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
                throw new SecurityException(ACCESS_DENIED + "allOf={"
                        + String.join(", ", permissions) + "}");
            }
        }
    }

    public void enforcePermissionAllOf(@NonNull String[] permissions,
            int pid, int uid) throws SecurityException {
        if (anyAppOps(permissions)) {
            AttributionSource source = new AttributionSource(uid, null, null);
            enforcePermissionAllOf(permissions, source);
            return;
        }
        for (String permission : permissions) {
            int result = checkPermission(permission, pid, uid);
            if (result != PermissionCheckerManager.PERMISSION_GRANTED) {
                throw new SecurityException(ACCESS_DENIED + "allOf={"
                        + String.join(", ", permissions) + "}");
            }
        }
    }

    public void enforcePermissionAnyOf(@NonNull String[] permissions,
            @NonNull AttributionSource source) throws SecurityException {
        for (String permission : permissions) {
            int result = checkPermission(permission, source);
            if (result == PermissionCheckerManager.PERMISSION_GRANTED) {
                return;
            }
        }
        throw new SecurityException(ACCESS_DENIED + "anyOf={"
                + String.join(", ", permissions) + "}");
    }

    public void enforcePermissionAnyOf(@NonNull String[] permissions,
            int pid, int uid) throws SecurityException {
        if (anyAppOps(permissions)) {
            AttributionSource source = new AttributionSource(uid, null, null);
            enforcePermissionAnyOf(permissions, source);
            return;
        }
        for (String permission : permissions) {
            int result = checkPermission(permission, pid, uid);
            if (result == PermissionCheckerManager.PERMISSION_GRANTED) {
                return;
            }
        }
        throw new SecurityException(ACCESS_DENIED + "anyOf={"
                + String.join(", ", permissions) + "}");
    }

    /**
     * Returns a new PermissionEnforcer based on a Context.
     *
     * @hide
     */
    public static PermissionEnforcer fromContext(@NonNull Context context) {
        return (PermissionEnforcer) context.getSystemService(Context.PERMISSION_ENFORCER_SERVICE);
    }
}
