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

Commit 4f6f70cd authored by Ytai Ben-Tsvi's avatar Ytai Ben-Tsvi
Browse files

Extract permission checking as a separate aspect

Permission checking logic is about to get more complicated, so this
change extracts it to a separate aspect class, where it doesn't get
mixed up with validation logic.

Change-Id: Id1c718fdd233244537c51ec1802a25571671c91c
Bug: 163865561
parent 2c344c0f
Loading
Loading
Loading
Loading
+267 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.soundtrigger_middleware;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ModelParameterRange;
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
import android.media.soundtrigger_middleware.PhraseSoundModel;
import android.media.soundtrigger_middleware.RecognitionConfig;
import android.media.soundtrigger_middleware.RecognitionEvent;
import android.media.soundtrigger_middleware.SoundModel;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.media.soundtrigger_middleware.Status;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;

import java.io.PrintWriter;
import java.util.Objects;

/**
 * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions.
 * <p>
 * Every public method in this class, overriding an interface method, must follow the following
 * pattern:
 * <code><pre>
 * @Override public T method(S arg) {
 *     // Permission check.
 *     checkPermissions();
 *     return mDelegate.method(arg);
 * }
 * </pre></code>
 *
 * {@hide}
 */
public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddlewareInternal, Dumpable {
    private static final String TAG = "SoundTriggerMiddlewarePermission";

    private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
    private final @NonNull Context mContext;

    public SoundTriggerMiddlewarePermission(
            @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
        mDelegate = delegate;
        mContext = context;
    }

    @Override
    public @NonNull
    SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
        checkPermissions();
        return mDelegate.listModules();
    }

    @Override
    public @NonNull
    ISoundTriggerModule attach(int handle,
            @NonNull ISoundTriggerCallback callback) throws RemoteException {
        checkPermissions();
        return new ModuleWrapper(
                mDelegate.attach(handle, new CallbackWrapper(callback)));
    }

    @Override
    public void setCaptureState(boolean active) throws RemoteException {
        // This is an internal call. No permissions needed.
        mDelegate.setCaptureState(active);
    }

    // Override toString() in order to have the delegate's ID in it.
    @Override
    public String toString() {
        return mDelegate.toString();
    }

    @Override
    public IBinder asBinder() {
        throw new UnsupportedOperationException(
                "This implementation is not inteded to be used directly with Binder.");
    }

    /**
     * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
     * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
     * caller temporarily doesn't have the right permissions to use this service.
     */
    private void checkPermissions() {
        enforcePermission(Manifest.permission.RECORD_AUDIO);
        enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
    }

    /**
     * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
     * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
     * caller temporarily doesn't have the given permission.
     *
     * @param permission The permission to check.
     */
    private void enforcePermission(String permission) {
        final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
                permission);
        switch (status) {
            case PermissionChecker.PERMISSION_GRANTED:
                return;
            case PermissionChecker.PERMISSION_HARD_DENIED:
                throw new SecurityException(
                        String.format("Caller must have the %s permission.", permission));
            case PermissionChecker.PERMISSION_SOFT_DENIED:
                throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
                        String.format("Caller must have the %s permission.", permission));
            default:
                throw new RuntimeException("Unexpected perimission check result.");
        }
    }

    @Override
    public void dump(PrintWriter pw) {
        if (mDelegate instanceof Dumpable) {
            ((Dumpable) mDelegate).dump(pw);
        }
    }

    /**
     * A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
     * mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
     */
    private class ModuleWrapper extends ISoundTriggerModule.Stub {
        private final ISoundTriggerModule mDelegate;

        ModuleWrapper(@NonNull ISoundTriggerModule delegate) {
            mDelegate = delegate;
        }

        @Override
        public int loadModel(@NonNull SoundModel model) throws RemoteException {
            checkPermissions();
            return mDelegate.loadModel(model);
        }

        @Override
        public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException {
            checkPermissions();
            return mDelegate.loadPhraseModel(model);
        }

        @Override
        public void unloadModel(int modelHandle) throws RemoteException {
            checkPermissions();
            mDelegate.unloadModel(modelHandle);

        }

        @Override
        public void startRecognition(int modelHandle, @NonNull RecognitionConfig config)
                throws RemoteException {
            checkPermissions();
            mDelegate.startRecognition(modelHandle, config);
        }

        @Override
        public void stopRecognition(int modelHandle) throws RemoteException {
            checkPermissions();
            mDelegate.stopRecognition(modelHandle);
        }

        @Override
        public void forceRecognitionEvent(int modelHandle) throws RemoteException {
            checkPermissions();
            mDelegate.forceRecognitionEvent(modelHandle);
        }

        @Override
        public void setModelParameter(int modelHandle, int modelParam, int value)
                throws RemoteException {
            checkPermissions();
            mDelegate.setModelParameter(modelHandle, modelParam, value);
        }

        @Override
        public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
            checkPermissions();
            return mDelegate.getModelParameter(modelHandle, modelParam);
        }

        @Override
        @Nullable
        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
                throws RemoteException {
            checkPermissions();
            return mDelegate.queryModelParameterSupport(modelHandle,
                    modelParam);
        }

        @Override
        public void detach() throws RemoteException {
            checkPermissions();
            mDelegate.detach();
        }

        // Override toString() in order to have the delegate's ID in it.
        @Override
        public String toString() {
            return Objects.toString(mDelegate);
        }
    }

    private class CallbackWrapper implements ISoundTriggerCallback {
        private final ISoundTriggerCallback mDelegate;

        private CallbackWrapper(ISoundTriggerCallback delegate) {
            mDelegate = delegate;
        }

        @Override
        public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException {
            mDelegate.onRecognition(modelHandle, event);
        }

        @Override
        public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
                throws RemoteException {
            mDelegate.onPhraseRecognition(modelHandle, event);
        }

        @Override
        public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
            mDelegate.onRecognitionAvailabilityChange(available);
        }

        @Override
        public void onModuleDied() throws RemoteException {
            mDelegate.onModuleDied();
        }

        @Override
        public IBinder asBinder() {
            return mDelegate.asBinder();
        }

        // Override toString() in order to have the delegate's ID in it.
        @Override
        public String toString() {
            return Objects.toString(mDelegate);
        }
    }
}
+6 −4
Original line number Diff line number Diff line
@@ -93,7 +93,8 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
        return new ModuleService(mDelegate.attach(handle, callback));
    }

    @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
    @Override
    protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
        if (mDelegate instanceof Dumpable) {
            ((Dumpable) mDelegate).dump(fout);
        }
@@ -182,9 +183,10 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
            publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
                    new SoundTriggerMiddlewareService(
                            new SoundTriggerMiddlewareLogging(
                                    new SoundTriggerMiddlewarePermission(
                                            new SoundTriggerMiddlewareValidation(
                                                    new SoundTriggerMiddlewareImpl(factories,
                                                    new AudioSessionProviderImpl()),
                                                            new AudioSessionProviderImpl())),
                                            getContext()))));
        }
    }
+7 −70
Original line number Diff line number Diff line
@@ -53,9 +53,9 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions and
 * correct usage by the client, as well as makes sure that exceptions representing a server
 * malfunction do not get sent to the client.
 * This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces correct usage by
 * the client, as well as makes sure that exceptions representing a server malfunction get sent to
 * the client in a consistent manner, which cannot be confused with a client fault.
 * <p>
 * This is intended to extract the non-business logic out of the underlying implementation and thus
 * make it easier to maintain each one of those separate aspects. A design trade-off is being made
@@ -70,8 +70,6 @@ import java.util.concurrent.atomic.AtomicReference;
 * pattern:
 * <code><pre>
 * @Override public T method(S arg) {
 *     // Permission check.
 *     checkPermissions();
 *     // Input validation.
 *     ValidationUtil.validateS(arg);
 *     synchronized (this) {
@@ -95,9 +93,8 @@ import java.util.concurrent.atomic.AtomicReference;
 * with client-server separation.
 * <p>
 * <b>Exception handling approach:</b><br>
 * We make sure all client faults (permissions, argument and state validation) happen first, and
 * would throw {@link SecurityException}, {@link IllegalArgumentException}/
 * {@link NullPointerException} or {@link
 * We make sure all client faults (argument and state validation) happen first, and
 * would throw {@link IllegalArgumentException}/{@link NullPointerException} or {@link
 * IllegalStateException}, respectively. All those exceptions are treated specially by Binder and
 * will get sent back to the client.<br>
 * Once this is done, any subsequent fault is considered either a recoverable (expected) or
@@ -130,13 +127,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
    private AtomicReference<Boolean> mCaptureState = new AtomicReference<>();

    private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
    private final @NonNull Context mContext;
    private Map<Integer, ModuleState> mModules;

    public SoundTriggerMiddlewareValidation(
            @NonNull ISoundTriggerMiddlewareInternal delegate, @NonNull Context context) {
            @NonNull ISoundTriggerMiddlewareInternal delegate) {
        mDelegate = delegate;
        mContext = context;
    }

    /**
@@ -169,8 +164,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
    @Override
    public @NonNull
    SoundTriggerModuleDescriptor[] listModules() {
        // Permission check.
        checkPermissions();
        // Input validation (always valid).

        synchronized (this) {
@@ -193,8 +186,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
    @Override
    public @NonNull ISoundTriggerModule attach(int handle,
            @NonNull ISoundTriggerCallback callback) {
        // Permission check.
        checkPermissions();
        // Input validation.
        Objects.requireNonNull(callback);
        Objects.requireNonNull(callback.asBinder());
@@ -244,40 +235,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
        return mDelegate.toString();
    }

    /**
     * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
     * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
     * caller temporarily doesn't have the right permissions to use this service.
     */
    void checkPermissions() {
        enforcePermission(Manifest.permission.RECORD_AUDIO);
        enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
    }

    /**
     * Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
     * or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
     * caller temporarily doesn't have the given permission.
     *
     * @param permission The permission to check.
     */
    void enforcePermission(String permission) {
        final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
                permission);
        switch (status) {
            case PermissionChecker.PERMISSION_GRANTED:
                return;
            case PermissionChecker.PERMISSION_HARD_DENIED:
                throw new SecurityException(
                        String.format("Caller must have the %s permission.", permission));
            case PermissionChecker.PERMISSION_SOFT_DENIED:
                throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
                        String.format("Caller must have the %s permission.", permission));
            default:
                throw new RuntimeException("Unexpected perimission check result.");
        }
    }

    @Override
    public IBinder asBinder() {
        throw new UnsupportedOperationException(
@@ -431,8 +388,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public int loadModel(@NonNull SoundModel model) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validateGenericModel(model);

@@ -455,8 +410,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public int loadPhraseModel(@NonNull PhraseSoundModel model) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validatePhraseModel(model);

@@ -479,8 +432,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void unloadModel(int modelHandle) {
            // Permission check.
            checkPermissions();
            // Input validation (always valid).

            synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -510,8 +461,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validateRecognitionConfig(config);

@@ -547,8 +496,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void stopRecognition(int modelHandle) {
            // Permission check.
            checkPermissions();
            // Input validation (always valid).

            synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -575,8 +522,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void forceRecognitionEvent(int modelHandle) {
            // Permission check.
            checkPermissions();
            // Input validation (always valid).

            synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -602,8 +547,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void setModelParameter(int modelHandle, int modelParam, int value) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validateModelParameter(modelParam);

@@ -630,8 +573,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public int getModelParameter(int modelHandle, int modelParam) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validateModelParameter(modelParam);

@@ -659,8 +600,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
        @Override
        @Nullable
        public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) {
            // Permission check.
            checkPermissions();
            // Input validation.
            ValidationUtil.validateModelParameter(modelParam);

@@ -689,8 +628,6 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware

        @Override
        public void detach() {
            // Permission check.
            checkPermissions();
            // Input validation (always valid).

            synchronized (SoundTriggerMiddlewareValidation.this) {
@@ -714,7 +651,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
        // Override toString() in order to have the delegate's ID in it.
        @Override
        public String toString() {
            return Objects.toString(mDelegate.toString());
            return Objects.toString(mDelegate);
        }

        private void detachInternal() {