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

Commit 889698a8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add AppOpsSession for data delivery management" into main

parents 1f819add 6a30ef5d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -128,8 +128,8 @@ BinderResult<bool> NativePermissionController::validateUidPackagePair(
    uid = uid % AID_USER_OFFSET;
    const auto fixed_package_opt = getFixedPackageName(uid);
    if (fixed_package_opt.has_value()) {
        return (uid == AID_ROOT || uid == AID_SYSTEM) ? true :
                packageName == fixed_package_opt.value();
        return (uid == AID_ROOT || uid == AID_SYSTEM) ? true
                                                      : packageName == fixed_package_opt.value();
    }
    std::lock_guard l{m_};
    if (!is_package_populated_) {
+157 −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.
 */

#pragma once

#include <android-base/thread_annotations.h>
#include <log/log.h>
#include <media/ValidatedAttributionSourceState.h>
#include <utils/RefBase.h>

#include <functional>

namespace android::media::permission {

using ValidatedAttributionSourceState =
        com::android::media::permission::ValidatedAttributionSourceState;

struct Ops {
    int attributedOp = -1;
    int additionalOp = -1;
};

/**
 * This session manages an ongoing data access corresponding with appops.
 *
 * This access can be temporarily stopped by appops or the data source. When access is revoked by
 * AppOps, the registered callback will be called in order to ensure that the data delivery is
 * halted. When halted by the data source, AppOps will be notified that the access ended.
 * Note, this session does not ref-count on itself. It should represent a single access, which
 * necessarily cannot nest.
 * This class is fully locked since notifications from appops are async. Public interface can be
 * slow due to binder calls.
 */
template <typename AppOpsFacade>
// Abstract interface that permits minor differences in how appops is called per client usage
    requires requires(AppOpsFacade x, const ValidatedAttributionSourceState attr) {
        { x.startAccess(attr, Ops{}) } -> std::same_as<bool>;  // true if permitted
        { x.stopAccess(attr, Ops{}) } -> std::same_as<void>;
        { x.checkAccess(attr, Ops{}) } -> std::same_as<bool>;  // true if permitted
        {
            x.addChangeCallback(attr, Ops{}, std::function<void(bool)>{})
        } -> std::same_as<uintptr_t>;
        // no more calls after return is required
        { x.removeChangeCallback(uintptr_t{}) } -> std::same_as<void>;
    }
class AppOpsSession {
  public:
    /**
     * @param attr - AttributionChain which the access is attributed to.
     * @param ops - The ops required for this delivery
     * @param opChangedCb - A callback (async) which  notifies the data source that the permitted
     * state due to appops has changed. This is only called if a delivery request is ongoing (i.e.
     * after a `beginDeliveryRequest` but before a `endDeliveryRequest`, regardless of the return
     * value of the former). Upon calling the cb, appops has been updated, so the post-condition is
     * that the data source delivers data iff the parameter is true. If the delivery fails for some
     * reason, `endDeliveryRequest` should be called shortly, however, there is no re-entrancy into
     * this class. The client should never change the access request state based on this cb.
     * @param appOpsFacade - See the requires clause -- an interface which encapsulates the calls to
     * AppOpsService.
     */
    AppOpsSession(ValidatedAttributionSourceState attr, Ops ops,
                  std::function<void(bool)> opChangedCb, AppOpsFacade appOpsFacade = {})
        : mAttr(std::move(attr)),
          mOps(ops),
          mCb(std::move(opChangedCb)),
          mAppOps(std::move(appOpsFacade)),
          mCookie(mAppOps.addChangeCallback(attr, ops,
                                            [this](bool x) { this->onPermittedChanged(x); })),
          mDeliveryRequested(false),
          mDeliveryPermitted(mAppOps.checkAccess(attr, ops)) {}

    ~AppOpsSession() {
        endDeliveryRequest();
        mAppOps.removeChangeCallback(mCookie);
    }

    /**
     * Source intends to start delivering data. Updates AppOps if applicable.
     * @return true if data should be delivered (i.e. AppOps also permits delivery)
     */
    bool beginDeliveryRequest() {
        std::lock_guard l{mLock};
        if (mDeliveryRequested) {
            ALOG(LOG_WARN, "AppOpsSession", "Redundant beginDeliveryRequest ignored");
            return mDeliveryPermitted;
        }
        mDeliveryRequested = true;
        if (mDeliveryPermitted) {
            mDeliveryPermitted = mAppOps.startAccess(mAttr, mOps);
        }
        return mDeliveryPermitted;
    }

    /**
     * Source intends to stop delivering data. Updates AppOps if applicable.
     */
    void endDeliveryRequest() {
        std::lock_guard l{mLock};
        if (!mDeliveryRequested) return;
        mDeliveryRequested = false;
        if (mDeliveryPermitted) {
            mAppOps.stopAccess(mAttr, mOps);
        }
    }

    /**
     * Check if delivery is permitted.
     */
    bool isDeliveryPermitted() const {
        std::lock_guard l{mLock};
        return mDeliveryPermitted;
    }

  private:
    /**
     * AppOps permitted state has changed. From callback thread.
     */
    void onPermittedChanged(bool isPermitted) {
        std::lock_guard l{mLock};
        if (mDeliveryPermitted == isPermitted) return;
        const bool oldIsPermitted = mDeliveryPermitted;
        mDeliveryPermitted = isPermitted;
        if (!mDeliveryRequested) return;
        if (mDeliveryPermitted) {
            mDeliveryPermitted = mAppOps.startAccess(mAttr, mOps);
        } else {
            mAppOps.stopAccess(mAttr, mOps);
        }
        if (oldIsPermitted != mDeliveryPermitted) {
            mCb(mDeliveryPermitted);
        }
    }

    mutable std::mutex mLock{};
    const ValidatedAttributionSourceState mAttr;
    const Ops mOps;
    const std::function<void(bool)> mCb;
    AppOpsFacade mAppOps GUARDED_BY(mLock);
    const uintptr_t mCookie;
    bool mDeliveryRequested GUARDED_BY(mLock);
    bool mDeliveryPermitted GUARDED_BY(mLock);
};

}  // namespace com::android::media::permission
+261 −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.
 */

#include <media/AppOpsSession.h>
#include <media/ValidatedAttributionSourceState.h>

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <functional>

using ::android::content::AttributionSourceState;
using ::android::media::permission::AppOpsSession;
using ::android::media::permission::Ops;
using ::com::android::media::permission::ValidatedAttributionSourceState;

using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Ne;
using ::testing::IsEmpty;

class AppOpsSessionTests;

class AppOpsTestFacade {
    friend AppOpsSessionTests;

  public:
    bool startAccess(const ValidatedAttributionSourceState&, Ops) {
        if (allowed_) ++running_;
        return allowed_;
    }

    void stopAccess(const ValidatedAttributionSourceState&, Ops) { --running_; }

    bool checkAccess(const ValidatedAttributionSourceState&, Ops) { return allowed_; }

    uintptr_t addChangeCallback(const ValidatedAttributionSourceState&, Ops,
                                std::function<void(bool)> cb) {
        cb_ = cb;
        return 42;
    }

    void removeChangeCallback(uintptr_t) {}

  private:
    // Static abuse since this is copied into the test, and represents "global" state
    static inline std::function<void(bool)> cb_;
    static inline bool allowed_;
    static inline int running_;
};

class AppOpsSessionTests : public ::testing::Test {
  protected:
    static constexpr Ops mOps = {100, 101};

    // We must manually clear the facade state, since it is static, unlike the members of this
    // class, since the fixture is constructed per-test.
    void SetUp() override {
        AppOpsTestFacade::cb_ = nullptr;
        AppOpsTestFacade::running_ = 0;
        AppOpsTestFacade::allowed_ = false;
    }

    void facadeSetAllowed(bool isAllowed) { AppOpsTestFacade::allowed_ = isAllowed; }

    int facadeGetRunning() { return AppOpsTestFacade::running_; }

    void facadeTriggerChange(bool isPermitted) {
        EXPECT_THAT(isPermitted, Ne(AppOpsTestFacade::allowed_));
        facadeSetAllowed(isPermitted);
        AppOpsTestFacade::cb_(isPermitted);
    }

    // Trigger a change callback, but without modifying the underlying state.
    // Allows for simulating a callback which is reversed quickly and callbacks which may not
    // apply to our package.
    void facadeTriggerSpuriousChange(bool isPermitted) { facadeSetAllowed(isPermitted); }

    void dataDeliveryCb(bool shouldDeliver) { mDeliveredCbs.push_back(shouldDeliver); }

    const AttributionSourceState mAttr = []() {
        AttributionSourceState attr;
        attr.uid = 1;
        attr.pid = 2;
        attr.deviceId = 3;
        return attr;
    }();

    void initSession() {
        mAppOpsSession.emplace(
                ValidatedAttributionSourceState::createFromTrustedSource(mAttr), mOps,
                [this](bool x) { dataDeliveryCb(x); }, AppOpsTestFacade{});
    }

    // For verification of delivered callbacks
    // vector<bool> since it's a test
    std::vector<bool> mDeliveredCbs;
    std::optional<AppOpsSession<AppOpsTestFacade>> mAppOpsSession;
};

TEST_F(AppOpsSessionTests, beginDeliveryRequest_Allowed) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, beginDeliveryRequest_Denied) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, endDeliveryRequest_Ongoing) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);
    mAppOpsSession->endDeliveryRequest();
    EXPECT_EQ(facadeGetRunning(), 0);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, endDeliveryRequest_Paused) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);
    mAppOpsSession->endDeliveryRequest();
    EXPECT_EQ(facadeGetRunning(), 0);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, endDeliveryRequest_PausedByCb) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);
    facadeTriggerChange(false);
    EXPECT_EQ(facadeGetRunning(), 0);

    mAppOpsSession->endDeliveryRequest();
    EXPECT_EQ(facadeGetRunning(), 0);
}

TEST_F(AppOpsSessionTests, onPermittedFalse_Ongoing_Change) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);
    facadeTriggerChange(false);
    EXPECT_EQ(facadeGetRunning(), 0);
    EXPECT_THAT(mDeliveredCbs, ElementsAreArray({false}));
}

TEST_F(AppOpsSessionTests, onPermittedTrue_Ongoing_Change) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);
    facadeTriggerChange(true);
    EXPECT_EQ(facadeGetRunning(), 1);
    EXPECT_THAT(mDeliveredCbs, ElementsAreArray({true}));
}

TEST_F(AppOpsSessionTests, onPermittedTrue_Ongoing_Change_Spurious) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);
    facadeTriggerSpuriousChange(true);
    EXPECT_EQ(facadeGetRunning(), 0);
    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, onPermittedFalse_Ongoing_Same) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);
    facadeTriggerSpuriousChange(false);
    EXPECT_EQ(facadeGetRunning(), 0);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, onPermittedTrue_Ongoing_Same) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);
    facadeTriggerSpuriousChange(true);
    EXPECT_EQ(facadeGetRunning(), 1);

    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, onPermittedFalse_Paused_Change) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    mAppOpsSession->endDeliveryRequest();

    EXPECT_EQ(facadeGetRunning(), 0);
    facadeTriggerChange(false);
    EXPECT_EQ(facadeGetRunning(), 0);
    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, onPermittedTrue_Paused_Change) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    mAppOpsSession->endDeliveryRequest();

    facadeTriggerChange(true);
    EXPECT_EQ(facadeGetRunning(), 0);
    EXPECT_THAT(mDeliveredCbs, IsEmpty());
}

TEST_F(AppOpsSessionTests, dtor_Running) {
    facadeSetAllowed(true);
    initSession();
    EXPECT_TRUE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 1);

    // call dtor
    mAppOpsSession.reset();
    EXPECT_EQ(facadeGetRunning(), 0);
}

TEST_F(AppOpsSessionTests, dtor_NotRunning) {
    facadeSetAllowed(false);
    initSession();
    EXPECT_FALSE(mAppOpsSession->beginDeliveryRequest());
    EXPECT_EQ(facadeGetRunning(), 0);

    // call dtor
    mAppOpsSession.reset();
    EXPECT_EQ(facadeGetRunning(), 0);
}