Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow subscriptions if wildcard expansion is empty. #34983

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 30 additions & 27 deletions src/app/InteractionModelEngine.cpp
Original file line number Diff line number Diff line change
@@ -463,14 +463,15 @@ Status InteractionModelEngine::OnInvokeCommandRequest(Messaging::ExchangeContext

CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDescriptor & aSubjectDescriptor,
AttributePathIBs::Parser & aAttributePathListParser,
bool & aHasValidAttributePath, size_t & aRequestedAttributePathCount)
PathInformation & aPathInfo)
{
TLV::TLVReader pathReader;
aAttributePathListParser.GetReader(&pathReader);
CHIP_ERROR err = CHIP_NO_ERROR;

aHasValidAttributePath = false;
aRequestedAttributePathCount = 0;
aPathInfo.isEmptyExpansion = true;
aPathInfo.hasValidPath = false;
aPathInfo.pathCount = 0;

while (CHIP_NO_ERROR == (err = pathReader.Next(TLV::AnonymousTag())))
{
@@ -494,6 +495,7 @@ CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDesc
// AttributePathExpandIterator. So we just need to check the ACL bits.
for (; pathIterator.Get(readPath); pathIterator.Next())
{
aPathInfo.isEmptyExpansion = false;
// leave requestPath.entityId optional value unset to indicate wildcard
Access::RequestPath requestPath{ .cluster = readPath.mClusterId,
.endpoint = readPath.mEndpointId,
@@ -502,7 +504,7 @@ CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDesc
RequiredPrivilege::ForReadAttribute(readPath));
if (err == CHIP_NO_ERROR)
{
aHasValidAttributePath = true;
aPathInfo.hasValidPath = true;
break;
}
}
@@ -511,6 +513,7 @@ CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDesc
{
ConcreteAttributePath concretePath(paramsList.mValue.mEndpointId, paramsList.mValue.mClusterId,
paramsList.mValue.mAttributeId);
aPathInfo.isEmptyExpansion = false;
if (ConcreteAttributePathExists(concretePath))
{
Access::RequestPath requestPath{ .cluster = concretePath.mClusterId,
@@ -522,12 +525,11 @@ CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDesc
RequiredPrivilege::ForReadAttribute(concretePath));
if (err == CHIP_NO_ERROR)
{
aHasValidAttributePath = true;
aPathInfo.hasValidPath = true;
}
}
}

aRequestedAttributePathCount++;
aPathInfo.pathCount++;
}

if (err == CHIP_ERROR_END_OF_TLV)
@@ -638,15 +640,15 @@ static bool HasValidEventPathForEndpoint(EndpointId aEndpoint, const EventPathPa
}

CHIP_ERROR InteractionModelEngine::ParseEventPaths(const Access::SubjectDescriptor & aSubjectDescriptor,
EventPathIBs::Parser & aEventPathListParser, bool & aHasValidEventPath,
size_t & aRequestedEventPathCount)
EventPathIBs::Parser & aEventPathListParser, PathInformation & aPathInfo)
{
TLV::TLVReader pathReader;
aEventPathListParser.GetReader(&pathReader);
CHIP_ERROR err = CHIP_NO_ERROR;

aHasValidEventPath = false;
aRequestedEventPathCount = 0;
aPathInfo.hasValidPath = false;
aPathInfo.isEmptyExpansion = true;
aPathInfo.pathCount = 0;

while (CHIP_NO_ERROR == (err = pathReader.Next(TLV::AnonymousTag())))
{
@@ -656,9 +658,9 @@ CHIP_ERROR InteractionModelEngine::ParseEventPaths(const Access::SubjectDescript
EventPathParams eventPath;
ReturnErrorOnFailure(path.ParsePath(eventPath));

++aRequestedEventPathCount;
++aPathInfo.pathCount;

if (aHasValidEventPath)
if (aPathInfo.hasValidPath)
{
// Can skip all the rest of the checking.
continue;
@@ -668,21 +670,23 @@ CHIP_ERROR InteractionModelEngine::ParseEventPaths(const Access::SubjectDescript
// access". We need to do some expansion of wildcards to handle that.
if (eventPath.HasWildcardEndpointId())
{
for (uint16_t endpointIndex = 0; !aHasValidEventPath && endpointIndex < emberAfEndpointCount(); ++endpointIndex)
for (uint16_t endpointIndex = 0; !aPathInfo.hasValidPath && endpointIndex < emberAfEndpointCount(); ++endpointIndex)
{
if (!emberAfEndpointIndexIsEnabled(endpointIndex))
{
continue;
}
aHasValidEventPath =
aPathInfo.isEmptyExpansion = false;
aPathInfo.hasValidPath =
HasValidEventPathForEndpoint(emberAfEndpointFromIndex(endpointIndex), eventPath, aSubjectDescriptor);
}
}
else
{
// No need to check whether the endpoint is enabled, because
// emberAfFindEndpointType returns null for disabled endpoints.
aHasValidEventPath = HasValidEventPathForEndpoint(eventPath.mEndpointId, eventPath, aSubjectDescriptor);
aPathInfo.isEmptyExpansion = false;
aPathInfo.hasValidPath = HasValidEventPathForEndpoint(eventPath.mEndpointId, eventPath, aSubjectDescriptor);
}
}

@@ -749,18 +753,16 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
}

{
size_t requestedAttributePathCount = 0;
size_t requestedEventPathCount = 0;
AttributePathIBs::Parser attributePathListParser;
bool hasValidAttributePath = false;
bool hasValidEventPath = false;

PathInformation attributePathInfo;
PathInformation eventPathInfo;

CHIP_ERROR err = subscribeRequestParser.GetAttributeRequests(&attributePathListParser);
if (err == CHIP_NO_ERROR)
{
auto subjectDescriptor = apExchangeContext->GetSessionHandle()->AsSecureSession()->GetSubjectDescriptor();
err = ParseAttributePaths(subjectDescriptor, attributePathListParser, hasValidAttributePath,
requestedAttributePathCount);
err = ParseAttributePaths(subjectDescriptor, attributePathListParser, attributePathInfo);
if (err != CHIP_NO_ERROR)
{
return Status::InvalidAction;
@@ -776,7 +778,7 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
if (err == CHIP_NO_ERROR)
{
auto subjectDescriptor = apExchangeContext->GetSessionHandle()->AsSecureSession()->GetSubjectDescriptor();
err = ParseEventPaths(subjectDescriptor, eventPathListParser, hasValidEventPath, requestedEventPathCount);
err = ParseEventPaths(subjectDescriptor, eventPathListParser, eventPathInfo);
if (err != CHIP_NO_ERROR)
{
return Status::InvalidAction;
@@ -787,7 +789,7 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
return Status::InvalidAction;
}

if (requestedAttributePathCount == 0 && requestedEventPathCount == 0)
if (attributePathInfo.pathCount == 0 && eventPathInfo.pathCount == 0)
{
ChipLogError(InteractionModel,
"Subscription from [%u:" ChipLogFormatX64 "] has no attribute or event paths. Rejecting request.",
@@ -796,7 +798,8 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
return Status::InvalidAction;
}

if (!hasValidAttributePath && !hasValidEventPath)
if (!(attributePathInfo.hasValidPath || eventPathInfo.hasValidPath ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are valid paths, then we know the client has some level of access. If all paths are empty, we can instead check if the client has access to anything at all (i.e. there is some ACL entry referencing the client). Or we could check for read access to the Descriptor cluster. Arguably wildcard expansion implicitly requires read access to the descriptor cluster anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is descriptor cluster access sufficient to check? If so we could update to that.

Otherwise generically I can only say "this is a real client, some data could be accessible at some time in the future theoretically" so if that is the case, we should never actually deny this as we do here. But that undoes the original PR that allows auto-rejection.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more, maybe wildcard expansion should actually explicitly check for access to the descriptor cluster anyway? It's a little strange that we're allowing information from the Descriptor cluster to be revealed when access to it hasn't necessarily been granted. On the other hand starting to enforce this now might break existing ACLs out there. But if we're treating information from the Descriptor cluster as special in this way maybe we should make this explicit in the spec, e.g. by having read access to the descriptor cluster be implicitly granted on any endpoints the client can access.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while since I waded into the spec (would need @tcarmelveilleux or @bzbarsky-apple's input here), but wildcard expansion is a server-side operation, and doesn't require the client have descriptor access I'd think. It's reasonable I'd think for a client to not have privilege to the descriptor cluster, and upon expansion, the server realize this.

A client could have access to just say, OnOff cluster, and request a wildcard, and correctly, be returned just the OnOff cluster on all matching endpoints.

(attributePathInfo.isEmptyExpansion && eventPathInfo.isEmptyExpansion)))
{
ChipLogError(InteractionModel,
"Subscription from [%u:" ChipLogFormatX64 "] has no access at all. Rejecting request.",
@@ -806,8 +809,8 @@ Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest
}

// The following cast is safe, since we can only hold a few tens of paths in one request.
if (!EnsureResourceForSubscription(apExchangeContext->GetSessionHandle()->GetFabricIndex(), requestedAttributePathCount,
requestedEventPathCount))
if (!EnsureResourceForSubscription(apExchangeContext->GetSessionHandle()->GetFabricIndex(), attributePathInfo.pathCount,
eventPathInfo.pathCount))
{
return Status::PathsExhausted;
}
101 changes: 58 additions & 43 deletions src/app/InteractionModelEngine.h
Original file line number Diff line number Diff line change
@@ -418,6 +418,13 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
friend class SubscriptionResumptionSessionEstablisher;
using Status = Protocols::InteractionModel::Status;

struct PathInformation
{
bool isEmptyExpansion = true;
bool hasValidPath = false;
size_t pathCount = 0;
};

void OnDone(CommandResponseSender & apResponderObj) override;
void OnDone(CommandHandlerImpl & apCommandObj) override;
void OnDone(ReadHandler & apReadObj) override;
@@ -448,24 +455,22 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
*
* aRequestedAttributePathCount will be updated to reflect the number of attribute paths in the request.
*
*
*/
CHIP_ERROR ParseAttributePaths(const Access::SubjectDescriptor & aSubjectDescriptor,
AttributePathIBs::Parser & aAttributePathListParser, bool & aHasValidAttributePath,
size_t & aRequestedAttributePathCount);
AttributePathIBs::Parser & aAttributePathListParser, PathInformation & aPathInformation);

/**
* This parses the event path list to ensure it is well formed. If so, for each path in the list, it will expand to a list
* of concrete paths and walk each path to check if it has privileges to read that event.
* This parses the event path list to ensure it is well formed. If so, for each path in the list,
* it will expand to a list of concrete paths and walk each path to check if it has privileges to
* read that event.
*
* If there is AT LEAST one "existent path" (as the spec calls it) that has sufficient privilege, aHasValidEventPath
* will be set to true. Otherwise, it will be set to false.
* If there is AT LEAST one "existent path" (as the spec calls it) that has sufficient privilege,
* aHasValidEventPath will be set to true. Otherwise, it will be set to false.
*
* aRequestedEventPathCount will be updated to reflect the number of event paths in the request.
*/
static CHIP_ERROR ParseEventPaths(const Access::SubjectDescriptor & aSubjectDescriptor,
EventPathIBs::Parser & aEventPathListParser, bool & aHasValidEventPath,
size_t & aRequestedEventPathCount);
EventPathIBs::Parser & aEventPathListParser, PathInformation & aPathInformation);

/**
* Called when Interaction Model receives a Read Request message. Errors processing
@@ -487,8 +492,9 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,

/**
* Called when Interaction Model receives a Timed Request message. Errors processing
* the Timed Request are handled entirely within this function. The caller pre-sets status to failure and the callee is
* expected to set it to success if it does not want an automatic status response message to be sent.
* the Timed Request are handled entirely within this function. The caller pre-sets status to
* failure and the callee is expected to set it to success if it does not want an automatic
* status response message to be sent.
*/
CHIP_ERROR OnTimedRequest(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader,
System::PacketBufferHandle && aPayload, Protocols::InteractionModel::Status & aStatus);
@@ -561,40 +567,45 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
}

/**
* Verify and ensure (by killing oldest read handlers that make the resources used by the current fabric exceed the fabric
* quota)
* - If the subscription uses resources within the per subscription limit, this function will always success by evicting
* existing subscriptions.
* - If the subscription uses more than per subscription limit, this function will return PATHS_EXHAUSTED if we are running out
* of paths.
* Verify and ensure (by killing oldest read handlers that make the resources used by the current
* fabric exceed the fabric quota)
* - If the subscription uses resources within the per subscription limit, this function will
* always success by evicting existing subscriptions.
* - If the subscription uses more than per subscription limit, this function will return
* PATHS_EXHAUSTED if we are running out of paths.
*
* After the checks above, we will try to ensure we have a free Readhandler for processing the subscription.
* After the checks above, we will try to ensure we have a free Readhandler for processing the
* subscription.
*
* @retval true when we have enough resources for the incoming subscription, false if not.
*/
bool EnsureResourceForSubscription(FabricIndex aFabricIndex, size_t aRequestedAttributePathCount,
size_t aRequestedEventPathCount);

/**
* Verify and ensure (by killing oldest read handlers that make the resources used by the current fabric exceed the fabric
* quota) the resources for handling a new read transaction with the given resource requirments.
* - PASE sessions will be counted in a virtual fabric (i.e. kInvalidFabricIndex will be consided as a "valid" fabric in this
* function)
* - If the existing resources can serve this read transaction, this function will return Status::Success.
* - or if the resources used by read transactions in the fabric index meets the per fabric resource limit (i.e. 9 paths & 1
* read) after accepting this read request, this function will always return Status::Success by evicting existing read
* transactions from other fabrics which are using more than the guaranteed minimum number of read.
* - or if the resources used by read transactions in the fabric index will exceed the per fabric resource limit (i.e. 9 paths &
* 1 read) after accepting this read request, this function will return a failure status without evicting any existing
* transaction.
* - However, read transactions on PASE sessions won't evict any existing read transactions when we have already commissioned
* CHIP_CONFIG_MAX_FABRICS fabrics on the device.
* Verify and ensure (by killing oldest read handlers that make the resources used by the current
* fabric exceed the fabric quota) the resources for handling a new read transaction with the
* given resource requirments.
* - PASE sessions will be counted in a virtual fabric (i.e. kInvalidFabricIndex will be consided
* as a "valid" fabric in this function)
* - If the existing resources can serve this read transaction, this function will return
* Status::Success.
* - or if the resources used by read transactions in the fabric index meets the per fabric
* resource limit (i.e. 9 paths & 1 read) after accepting this read request, this function will
* always return Status::Success by evicting existing read transactions from other fabrics which
* are using more than the guaranteed minimum number of read.
* - or if the resources used by read transactions in the fabric index will exceed the per fabric
* resource limit (i.e. 9 paths & 1 read) after accepting this read request, this function will
* return a failure status without evicting any existing transaction.
* - However, read transactions on PASE sessions won't evict any existing read transactions when
* we have already commissioned CHIP_CONFIG_MAX_FABRICS fabrics on the device.
*
* @retval Status::Success: The read transaction can be accepted.
* @retval Status::Busy: The remaining resource is insufficient to handle this read request, and the accessing fabric for this
* read request will use more resources than we guaranteed, the client is expected to retry later.
* @retval Status::PathsExhausted: The attribute / event path pool is exhausted, and the read request is requesting more
* resources than we guaranteed.
* @retval Status::Busy: The remaining resource is insufficient to handle this read request, and
* the accessing fabric for this read request will use more resources than we guaranteed, the
* client is expected to retry later.
* @retval Status::PathsExhausted: The attribute / event path pool is exhausted, and the read
* request is requesting more resources than we guaranteed.
*/
Status EnsureResourceForRead(FabricIndex aFabricIndex, size_t aRequestedAttributePathCount, size_t aRequestedEventPathCount);

@@ -630,10 +641,12 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
#if !CHIP_SYSTEM_CONFIG_POOL_USE_HEAP
static_assert(CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_SUBSCRIPTIONS >=
CHIP_CONFIG_MAX_FABRICS * (kMinSupportedPathsPerSubscription * kMinSupportedSubscriptionsPerFabric),
"CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_SUBSCRIPTIONS is too small to match the requirements of spec 8.5.1");
"CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_SUBSCRIPTIONS is too small to match the "
"requirements of spec 8.5.1");
static_assert(CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_READS >=
CHIP_CONFIG_MAX_FABRICS * (kMinSupportedReadRequestsPerFabric * kMinSupportedPathsPerReadRequest),
"CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_READS is too small to match the requirements of spec 8.5.1");
"CHIP_IM_SERVER_MAX_NUM_PATH_GROUPS_FOR_READS is too small to match the "
"requirements of spec 8.5.1");
static_assert(CHIP_IM_MAX_NUM_SUBSCRIPTIONS >= CHIP_CONFIG_MAX_FABRICS * kMinSupportedSubscriptionsPerFabric,
"CHIP_IM_MAX_NUM_SUBSCRIPTIONS is too small to match the requirements of spec 8.5.1");
static_assert(CHIP_IM_MAX_NUM_READS >= CHIP_CONFIG_MAX_FABRICS * kMinSupportedReadRequestsPerFabric,
@@ -667,8 +680,9 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,

int mMaxNumFabricsOverride = -1;

// We won't limit the handler used per fabric on platforms that are using heap for memory pools, so we introduces a flag to
// enforce such check based on the configured size. This flag is used for unit tests only, there is another compare time flag
// We won't limit the handler used per fabric on platforms that are using heap for memory pools,
// so we introduces a flag to enforce such check based on the configured size. This flag is used
// for unit tests only, there is another compare time flag
// CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK for stress tests.
bool mForceHandlerQuota = false;
#if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS && CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION
@@ -678,10 +692,11 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,

#if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
/**
* mNumOfSubscriptionsToResume tracks the number of subscriptions that the device will try to resume at its next resumption
* attempt. At boot up, the attempt will be at the highest min interval of all the subscriptions to resume.
* When the subscription timeout resumption feature is present, after the boot up attempt, the next attempt will be determined
* by ComputeTimeSecondsTillNextSubscriptionResumption.
* mNumOfSubscriptionsToResume tracks the number of subscriptions that the device will try to
* resume at its next resumption attempt. At boot up, the attempt will be at the highest min
* interval of all the subscriptions to resume. When the subscription timeout resumption feature
* is present, after the boot up attempt, the next attempt will be determined by
* ComputeTimeSecondsTillNextSubscriptionResumption.
*/
int8_t mNumOfSubscriptionsToResume = 0;
#if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION
5 changes: 4 additions & 1 deletion src/app/tests/TestAclAttribute.cpp
Original file line number Diff line number Diff line change
@@ -187,7 +187,10 @@ TEST_F(TestAclAttribute, TestACLDeniedAttribute)
EXPECT_EQ(readClient.SendRequest(readPrepareParams), CHIP_NO_ERROR);

DrainAndServiceIO();
EXPECT_EQ(delegate.mError, CHIP_IM_GLOBAL_STATUS(InvalidAction));

// Wildcard subscriptions are ok: endpoint id is a wildcard
EXPECT_EQ(delegate.mError, CHIP_NO_ERROR);
// No report data since none of the current clusters have data
EXPECT_FALSE(delegate.mGotReport);
delegate.mError = CHIP_NO_ERROR;
delegate.mGotReport = false;
9 changes: 5 additions & 4 deletions src/controller/tests/data_model/TestRead.cpp
Original file line number Diff line number Diff line change
@@ -3176,7 +3176,7 @@ void EstablishReadOrSubscriptions(const SessionHandle & sessionHandle, size_t nu

} // namespace SubscriptionPathQuotaHelpers

TEST_F(TestRead, TestSubscribeAttributeDeniedNotExistPath)
TEST_F(TestRead, TestSubscribeAttributeNotExistPath)
{
auto sessionHandle = GetSessionBobToAlice();

@@ -3205,9 +3205,9 @@ TEST_F(TestRead, TestSubscribeAttributeDeniedNotExistPath)

DrainAndServiceIO();

EXPECT_EQ(callback.mOnError, 1u);
EXPECT_EQ(callback.mLastError, CHIP_IM_GLOBAL_STATUS(InvalidAction));
EXPECT_EQ(callback.mOnDone, 1u);
// If path does not exist, we allow the subscription (in case path appears over time)
EXPECT_EQ(callback.mOnError, 0u);
EXPECT_EQ(callback.mOnDone, 0u); // subscription is active, not done
}

SetMRPMode(chip::Test::MessagingContext::MRPMode::kDefault);
@@ -4708,6 +4708,7 @@ TEST_F(TestRead, TestReadHandler_KeepSubscriptionTest)
DrainAndServiceIO();

EXPECT_EQ(app::InteractionModelEngine::GetInstance()->GetNumActiveReadHandlers(), 0u);
// InvalidAction due to empty param list size, however subscriptions are cleared
EXPECT_NE(readCallback.mOnError, 0u);
app::InteractionModelEngine::GetInstance()->ShutdownActiveReads();
DrainAndServiceIO();