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

Commit 6a30ef5d authored by Atneya Nair's avatar Atneya Nair
Browse files

Add AppOpsSession for data delivery management

Test: atest AppOpsSessionTests
Bug: 355498020
Flag: EXEMPT, safe adding utils
Change-Id: Icdc6abe190a559292c9e7941fef47976438e123b
parent d9e00897
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);
}