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

Use DataModel::Provider provided command information to handle command validation #35897

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 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
9 changes: 6 additions & 3 deletions examples/lighting-app/tizen/src/DBusInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,12 @@ class CommandHandlerImplCallback : public CommandHandlerImpl::Callback
{
public:
using Status = Protocols::InteractionModel::Status;
void OnDone(CommandHandlerImpl & apCommandObj) {}
void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) {}
Status CommandExists(const ConcreteCommandPath & aCommandPath) { return Status::Success; }

void OnDone(CommandHandlerImpl & apCommandObj) override {}
void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath,
TLV::TLVReader & apPayload) override
{}
Status ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) override { return Status::Success; }
};

DBusInterface::DBusInterface(chip::EndpointId endpointId) : mEndpointId(endpointId)
Expand Down
1 change: 1 addition & 0 deletions src/app/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ source_set("command-handler-impl") {
"${chip_root}/src/access:types",
"${chip_root}/src/app/MessageDef",
"${chip_root}/src/app/data-model",
"${chip_root}/src/app/data-model-provider",
"${chip_root}/src/app/util:callbacks",
"${chip_root}/src/lib/support",
"${chip_root}/src/messaging",
Expand Down
85 changes: 25 additions & 60 deletions src/app/CommandHandlerImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <app/MessageDef/StatusIB.h>
#include <app/RequiredPrivilege.h>
#include <app/StatusResponse.h>
#include <app/data-model-provider/OperationTypes.h>
#include <app/util/MatterCallbacks.h>
#include <credentials/GroupDataProvider.h>
#include <lib/core/CHIPConfig.h>
Expand Down Expand Up @@ -390,55 +391,17 @@ Status CommandHandlerImpl::ProcessCommandDataIB(CommandDataIB::Parser & aCommand
VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction);

{
Status commandExists = mpCallback->CommandExists(concretePath);
if (commandExists != Status::Success)
{
ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x",
ChipLogValueMEI(concretePath.mCommandId), ChipLogValueMEI(concretePath.mClusterId),
concretePath.mEndpointId);
return FallibleAddStatus(concretePath, commandExists) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
}
}

{
Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor();
Access::RequestPath requestPath{ .cluster = concretePath.mClusterId,
.endpoint = concretePath.mEndpointId,
.requestType = Access::RequestType::kCommandInvokeRequest,
.entityId = concretePath.mCommandId };
Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath);
err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege);
if (err != CHIP_NO_ERROR)
{
if ((err != CHIP_ERROR_ACCESS_DENIED) && (err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL))
{
return FallibleAddStatus(concretePath, Status::Failure) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
}
// TODO: when wildcard invokes are supported, handle them to discard rather than fail with status
Status status = err == CHIP_ERROR_ACCESS_DENIED ? Status::UnsupportedAccess : Status::AccessRestricted;
return FallibleAddStatus(concretePath, status) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
}
}
DataModel::InvokeRequest request;

if (CommandNeedsTimedInvoke(concretePath.mClusterId, concretePath.mCommandId) && !IsTimedInvoke())
{
// TODO: when wildcard invokes are supported, discard a
// wildcard-expanded path instead of returning a status.
return FallibleAddStatus(concretePath, Status::NeedsTimedInteraction) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
}

if (CommandIsFabricScoped(concretePath.mClusterId, concretePath.mCommandId))
{
// SPEC: Else if the command in the path is fabric-scoped and there is no accessing fabric,
// a CommandStatusIB SHALL be generated with the UNSUPPORTED_ACCESS Status Code.
request.path = concretePath;
request.subjectDescriptor = GetSubjectDescriptor();
request.accessingFabricIndex = GetAccessingFabricIndex();
request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, IsTimedInvoke());

// Fabric-scoped commands are not allowed before a specific accessing fabric is available.
// This is mostly just during a PASE session before AddNOC.
if (GetAccessingFabricIndex() == kUndefinedFabricIndex)
Status preCheckStatus = mpCallback->ValidateCommandCanBeDispatched(request);
if (preCheckStatus != Status::Success)
{
// TODO: when wildcard invokes are supported, discard a
// wildcard-expanded path instead of returning a status.
return FallibleAddStatus(concretePath, Status::UnsupportedAccess) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
return FallibleAddStatus(concretePath, preCheckStatus) != CHIP_NO_ERROR ? Status::Failure : Status::Success;
}
}

Expand Down Expand Up @@ -516,11 +479,19 @@ Status CommandHandlerImpl::ProcessGroupCommandDataIB(CommandDataIB::Parser & aCo
// once up front and discard all the paths at once. Ordering with respect
// to ACL and command presence checks does not matter, because the behavior
// is the same for all of them: ignore the path.
#if !CHIP_CONFIG_USE_DATA_MODEL_INTERFACE

// Without data model interface, we can query individual commands.
// Data model interface queries commands by a full path so we need endpointID as well.
//
// Since this is a performance update and group commands are never timed,
// missing this should not be that noticeable.
if (CommandNeedsTimedInvoke(clusterId, commandId))
{
// Group commands are never timed.
return Status::Success;
}
#endif

// No check for `CommandIsFabricScoped` unlike in `ProcessCommandDataIB()` since group commands
// always have an accessing fabric, by definition.
Expand All @@ -542,30 +513,24 @@ Status CommandHandlerImpl::ProcessGroupCommandDataIB(CommandDataIB::Parser & aCo

const ConcreteCommandPath concretePath(mapping.endpoint_id, clusterId, commandId);

if (mpCallback->CommandExists(concretePath) != Status::Success)
{
ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x",
ChipLogValueMEI(commandId), ChipLogValueMEI(clusterId), mapping.endpoint_id);
DataModel::InvokeRequest request;

continue;
}
request.path = concretePath;
request.subjectDescriptor = GetSubjectDescriptor();
request.accessingFabricIndex = GetAccessingFabricIndex();
request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, IsTimedInvoke());

{
Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor();
Access::RequestPath requestPath{ .cluster = concretePath.mClusterId,
.endpoint = concretePath.mEndpointId,
.requestType = Access::RequestType::kCommandInvokeRequest,
.entityId = concretePath.mCommandId };
Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath);
err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege);
if (err != CHIP_NO_ERROR)
Status preCheckStatus = mpCallback->ValidateCommandCanBeDispatched(request);
andy31415 marked this conversation as resolved.
Show resolved Hide resolved
if (preCheckStatus != Status::Success)
{
// NOTE: an expected error is CHIP_ERROR_ACCESS_DENIED, but there could be other unexpected errors;
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment is no longer true. For one thing, there is no "error" here at all anymore.

// therefore, keep processing subsequent commands, and if any errors continue, those subsequent
// commands will likewise fail.
continue;
}
}

if ((err = DataModelCallbacks::GetInstance()->PreCommandReceived(concretePath, GetSubjectDescriptor())) == CHIP_NO_ERROR)
{
TLV::TLVReader dataReader(commandDataReader);
Expand Down
27 changes: 18 additions & 9 deletions src/app/CommandHandlerImpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <app/CommandPathRegistry.h>
#include <app/MessageDef/InvokeRequestMessage.h>
#include <app/MessageDef/InvokeResponseMessage.h>
#include <app/data-model-provider/OperationTypes.h>
#include <lib/core/TLV.h>
#include <lib/core/TLVDebug.h>
#include <lib/support/BitFlags.h>
Expand Down Expand Up @@ -50,21 +51,29 @@ class CommandHandlerImpl : public CommandHandler
*/
virtual void OnDone(CommandHandlerImpl & apCommandObj) = 0;

/**
* Perform pre-validation that the command dispatch can be performed. In particular:
* - check command existence/validity
* - validate ACL
* - validate timed-invoke and fabric-scoped requirements
*
* Returns Status::Success if the command can be dispatched, otherwise it will
* return the status to be forwarded to the client on failure.
*
* Possible error return codes:
* - UnsupportedEndpoint/UnsupportedCluster/UnsupportedCommand if the command path is invalid
* - NeedsTimedInteraction
* - UnsupportedAccess (ACL failure or fabric scoped without a valid fabric index)
* - AccessRestricted
*/
virtual Protocols::InteractionModel::Status ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) = 0;

/*
* Upon processing of a CommandDataIB, this method is invoked to dispatch the command
* to the right server-side handler provided by the application.
*/
virtual void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath,
TLV::TLVReader & apPayload) = 0;

/*
* Check to see if a command implementation exists for a specific
* concrete command path. If it does, Success will be returned. If
* not, one of UnsupportedEndpoint, UnsupportedCluster, or
* UnsupportedCommand will be returned, depending on how the command
* fails to exist.
*/
virtual Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) = 0;
};

struct InvokeResponseParameters
Expand Down
6 changes: 3 additions & 3 deletions src/app/CommandResponseSender.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ void CommandResponseSender::DispatchCommand(CommandHandlerImpl & apCommandObj, c
mpCommandHandlerCallback->DispatchCommand(apCommandObj, aCommandPath, apPayload);
}

Status CommandResponseSender::CommandExists(const ConcreteCommandPath & aCommandPath)
Protocols::InteractionModel::Status CommandResponseSender::ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request)
{
VerifyOrReturnValue(mpCommandHandlerCallback, Protocols::InteractionModel::Status::UnsupportedCommand);
return mpCommandHandlerCallback->CommandExists(aCommandPath);
VerifyOrReturnValue(mpCommandHandlerCallback, Protocols::InteractionModel::Status::Failure);
return mpCommandHandlerCallback->ValidateCommandCanBeDispatched(request);
}

CHIP_ERROR CommandResponseSender::SendCommandResponse()
Expand Down
2 changes: 1 addition & 1 deletion src/app/CommandResponseSender.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class CommandResponseSender : public Messaging::ExchangeDelegate,
void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath,
TLV::TLVReader & apPayload) override;

Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) override;
Protocols::InteractionModel::Status ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) override;

/**
* Gets the inner exchange context object, without ownership.
Expand Down
96 changes: 93 additions & 3 deletions src/app/InteractionModelEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
#include <app/AppConfig.h>
#include <app/CommandHandlerInterfaceRegistry.h>
#include <app/RequiredPrivilege.h>
#include <app/data-model-provider/ActionReturnStatus.h>
#include <app/data-model-provider/MetadataTypes.h>
#include <app/data-model-provider/OperationTypes.h>
#include <app/util/IMClusterCommandHandler.h>
#include <app/util/af-types.h>
#include <app/util/ember-compatibility-functions.h>
Expand All @@ -42,10 +45,9 @@
#include <lib/support/CHIPFaultInjection.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/FibonacciUtils.h>
#include <protocols/interaction_model/StatusCode.h>

#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
#include <app/data-model-provider/ActionReturnStatus.h>

// TODO: defaulting to codegen should eventually be an application choice and not
// hard-coded in the interaction model
#include <app/codegen-data-model-provider/Instance.h>
Expand Down Expand Up @@ -1646,6 +1648,8 @@ void InteractionModelEngine::DispatchCommand(CommandHandlerImpl & apCommandObj,

DataModel::InvokeRequest request;
request.path = aCommandPath;
request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, apCommandObj.IsTimedInvoke());
request.subjectDescriptor = apCommandObj.GetSubjectDescriptor();

std::optional<DataModel::ActionReturnStatus> status = GetDataModelProvider()->Invoke(request, apPayload, &apCommandObj);

Expand Down Expand Up @@ -1678,7 +1682,93 @@ void InteractionModelEngine::DispatchCommand(CommandHandlerImpl & apCommandObj,
#endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
}

Protocols::InteractionModel::Status InteractionModelEngine::CommandExists(const ConcreteCommandPath & aCommandPath)
Protocols::InteractionModel::Status InteractionModelEngine::ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request)
{

Status status = CommandCheckExists(request.path);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably be named CheckCommandExistence.


if (status != Status::Success)
{
ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x",
Copy link
Contributor

Choose a reason for hiding this comment

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

I know you just copied this, but endpoint ids should generally be decimal, not hex....

ChipLogValueMEI(request.path.mCommandId), ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId);
return status;
}

status = CommandCheckACL(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should probably be called CheckCommandAccess? Especially because it checks more than just ACL....

VerifyOrReturnValue(status == Status::Success, status);

return CommandCheckFlags(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

CheckCommandFlags.

}

Protocols::InteractionModel::Status InteractionModelEngine::CommandCheckACL(const DataModel::InvokeRequest & aRequest)
{
if (!aRequest.subjectDescriptor.has_value())
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is that subjectDescriptor field optional at all? That seems like very strange API.

I guess for some read/write bits right now it doesn't exit. But for commands it absolutely always must, and I think just aborting here if it does not is fine.

{
return Status::UnsupportedAccess; // we require a subject for invoke
}

Access::RequestPath requestPath{ .cluster = aRequest.path.mClusterId,
.endpoint = aRequest.path.mEndpointId,
.requestType = Access::RequestType::kCommandInvokeRequest,
.entityId = aRequest.path.mCommandId };
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
std::optional<DataModel::CommandInfo> commandInfo = mDataModelProvider->GetAcceptedCommandInfo(aRequest.path);
// This is checked by previous validations, so it should not happen
VerifyOrReturnValue(commandInfo.has_value(), Status::UnsupportedCommand);
Comment on lines +1715 to +1717
Copy link
Contributor

Choose a reason for hiding this comment

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

This is very much not going to work in a world where the ACL check has to come before the existence check (which is the world of the spec).

Can we please write this code so that it won't break when these two checks are reordered in the callsite?

Now how this is supposed to work... the spec does not in fact define it (needs a spec issue, note @tcarmelveilleux) but would think that you should default to Operate privilege for purposes of the ACL check if the command is not known.

Note that the Ember codepath in the #else does exactly that: if not known, uses Operate. We should not have behavior differences between the two codepaths.


Access::Privilege requestPrivilege = commandInfo->invokePrivilege;
andy31415 marked this conversation as resolved.
Show resolved Hide resolved
#else
Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(aRequest.path);
#endif
CHIP_ERROR err = Access::GetAccessControl().Check(*aRequest.subjectDescriptor, requestPath, requestPrivilege);
if (err != CHIP_NO_ERROR)
{
if ((err != CHIP_ERROR_ACCESS_DENIED) && (err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL))
{
return Status::Failure;
}
return err == CHIP_ERROR_ACCESS_DENIED ? Status::UnsupportedAccess : Status::AccessRestricted;
}

return Status::Success;
}

Protocols::InteractionModel::Status InteractionModelEngine::CommandCheckFlags(const DataModel::InvokeRequest & aRequest)
{
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
std::optional<DataModel::CommandInfo> commandInfo = mDataModelProvider->GetAcceptedCommandInfo(aRequest.path);
// This is checked by previous validations, so it should not happen
Copy link
Contributor

Choose a reason for hiding this comment

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

Enough "should not" that we should just abort if it does?

VerifyOrReturnValue(commandInfo.has_value(), Status::UnsupportedCommand);

const bool commandNeedsTimedInvoke = commandInfo->flags.Has(DataModel::CommandQualityFlags::kTimed);
const bool commandIsFabricScoped = commandInfo->flags.Has(DataModel::CommandQualityFlags::kFabricScoped);
#else
const bool commandNeedsTimedInvoke = CommandNeedsTimedInvoke(aRequest.path.mClusterId, aRequest.path.mCommandId);
const bool commandIsFabricScoped = CommandIsFabricScoped(aRequest.path.mClusterId, aRequest.path.mCommandId);
#endif

if (commandNeedsTimedInvoke && !aRequest.invokeFlags.Has(DataModel::InvokeFlags::kTimed))
{
return Status::NeedsTimedInteraction;
}

if (commandIsFabricScoped)
{
// SPEC: Else if the command in the path is fabric-scoped and there is no accessing fabric,
// a CommandStatusIB SHALL be generated with the UNSUPPORTED_ACCESS Status Code.

// Fabric-scoped commands are not allowed before a specific accessing fabric is available.
// This is mostly just during a PASE session before AddNOC.
if (aRequest.accessingFabricIndex == kUndefinedFabricIndex)
{
return Status::UnsupportedAccess;
}
}

return Status::Success;
}

Protocols::InteractionModel::Status InteractionModelEngine::CommandCheckExists(const ConcreteCommandPath & aCommandPath)
{
#if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE
auto provider = GetDataModelProvider();
Expand Down
7 changes: 6 additions & 1 deletion src/app/InteractionModelEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include <app/TimedHandler.h>
#include <app/WriteClient.h>
#include <app/WriteHandler.h>
#include <app/data-model-provider/OperationTypes.h>
#include <app/data-model-provider/Provider.h>
#include <app/icd/server/ICDServerConfig.h>
#include <app/reporting/Engine.h>
Expand Down Expand Up @@ -508,7 +509,7 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath,
TLV::TLVReader & apPayload) override;

Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) override;
Protocols::InteractionModel::Status ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) override;

bool HasActiveRead();

Expand Down Expand Up @@ -612,6 +613,10 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler,
void ShutdownMatchingSubscriptions(const Optional<FabricIndex> & aFabricIndex = NullOptional,
const Optional<NodeId> & aPeerNodeId = NullOptional);

Status CommandCheckExists(const ConcreteCommandPath & aCommandPath);
Status CommandCheckACL(const DataModel::InvokeRequest & aRequest);
Status CommandCheckFlags(const DataModel::InvokeRequest & aRequest);

/**
* Check if the given attribute path is a valid path in the data model provider.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/app/data-model-provider/OperationTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <access/SubjectDescriptor.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteCommandPath.h>
#include <lib/core/DataModelTypes.h>
#include <lib/support/BitFlags.h>

#include <cstdint>
Expand Down Expand Up @@ -99,6 +100,7 @@ struct InvokeRequest : OperationRequest
{
ConcreteCommandPath path;
BitFlags<InvokeFlags> invokeFlags;
FabricIndex accessingFabricIndex = kUndefinedFabricIndex;
Copy link
Contributor

Choose a reason for hiding this comment

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

I do have to ask: why do we need this if we have a subject descriptor member already? What we should have is a getter for the accessing fabric index, which gets it from our subject descriptor, instead of having duplicated members.

};

} // namespace DataModel
Expand Down
Loading
Loading