blob: 4f56243a0c549582a6f0ff1d795a9f2cc75a2a6d [file] [log] [blame]
/**
*
* Copyright (c) 2023-2025 Project CHIP Authors
*
* 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 <app/clusters/icd-management-server/ICDManagementCluster.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/IcdManagement/ClusterId.h>
#if CHIP_CONFIG_ENABLE_ICD_CIP
#include <access/AccessControl.h>
#include <app/icd/server/ICDMonitoringTable.h> // nogncheck
#include <app/icd/server/ICDNotifier.h> // nogncheck
#endif
#if CHIP_CONFIG_ENABLE_ICD_SERVER
#include <app/server/Server.h> // nogncheck
#endif
using namespace chip;
using namespace chip::app;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::IcdManagement;
using namespace Protocols;
using namespace chip::Access;
using chip::Protocols::InteractionModel::Status;
namespace {
#if CHIP_CONFIG_ENABLE_ICD_CIP
/**
* @brief Function checks if the client has admin permissions to the cluster in the commandPath
*
* @param[out] isClientAdmin True : Client has admin permissions
* False : Client does not have admin permissions
* If an error occurs, isClientAdmin is not changed
* @return CHIP_ERROR
*/
CHIP_ERROR CheckAdmin(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, bool & isClientAdmin)
{
RequestPath requestPath{ .cluster = commandPath.mClusterId,
.endpoint = commandPath.mEndpointId,
.requestType = RequestType::kCommandInvokeRequest,
.entityId = commandPath.mCommandId };
CHIP_ERROR err = GetAccessControl().Check(commandObj->GetSubjectDescriptor(), requestPath, Privilege::kAdminister);
if (CHIP_NO_ERROR == err)
{
isClientAdmin = true;
}
else if (CHIP_ERROR_ACCESS_DENIED == err)
{
isClientAdmin = false;
err = CHIP_NO_ERROR;
}
return err;
}
#endif // CHIP_CONFIG_ENABLE_ICD_CIP
} // anonymous namespace
namespace chip::app::Clusters {
#if CHIP_CONFIG_ENABLE_ICD_CIP
void ICDManagementFabricDelegate::Init(PersistentStorageDelegate & storage, Crypto::SymmetricKeystore * symmetricKeystore,
ICDConfigurationData & icdConfigurationData)
{
mStorage = &storage;
mSymmetricKeystore = symmetricKeystore;
mICDConfigurationData = &icdConfigurationData;
}
void ICDManagementFabricDelegate::OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex)
{
uint16_t supported_clients = mICDConfigurationData->GetClientsSupportedPerFabric();
ICDMonitoringTable table(*mStorage, fabricIndex, supported_clients, mSymmetricKeystore);
TEMPORARY_RETURN_IGNORED table.RemoveAll();
ICDNotifier::GetInstance().NotifyICDManagementEvent(ICDListener::ICDManagementEvents::kTableUpdated);
}
#endif // CHIP_CONFIG_ENABLE_ICD_CIP
ICDManagementCluster::ICDManagementCluster(EndpointId endpointId, Crypto::SymmetricKeystore & symmetricKeystore,
FabricTable & fabricTable, ICDConfigurationData & icdConfigurationData,
OptionalAttributeSet optionalAttributeSet,
BitMask<IcdManagement::OptionalCommands> enabledCommands,
BitMask<IcdManagement::UserActiveModeTriggerBitmap> userActiveModeTriggerBitmap,
CharSpan userActiveModeTriggerInstruction) :
DefaultServerCluster({ endpointId, IcdManagement::Id }),
mSymmetricKeystore(symmetricKeystore), mFabricTable(fabricTable), mICDConfigurationData(icdConfigurationData),
mOptionalAttributeSet(optionalAttributeSet), mUserActiveModeTriggerBitmap(userActiveModeTriggerBitmap),
mEnabledCommands(enabledCommands), mUserActiveModeTriggerInstructionLength(0)
{
static_assert(sizeof(mUserActiveModeTriggerInstruction) <= UINT8_MAX,
"mUserActiveModeTriggerInstruction size must fit in uint8_t");
MutableCharSpan buffer(mUserActiveModeTriggerInstruction);
CopyCharSpanToMutableCharSpanWithTruncation(userActiveModeTriggerInstruction, buffer);
mUserActiveModeTriggerInstructionLength = static_cast<uint8_t>(buffer.size());
}
CHIP_ERROR ICDManagementCluster::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(DefaultServerCluster::Startup(context));
// TODO(#32321): Remove #if after issue is resolved
// Note: We only need this #if statement for platform examples that enable the ICD management server without building the sample
// as an ICD. Since this is not spec compliant, we should remove this #if statement once we stop compiling the ICD management
// server in those examples.
#if CHIP_CONFIG_ENABLE_ICD_SERVER
Server::GetInstance().GetICDManager().RegisterObserver(this);
#endif
return CHIP_NO_ERROR;
}
void ICDManagementCluster::Shutdown(ClusterShutdownType shutdownType)
{
// TODO(#32321): Remove #if after issue is resolved
// Note: We only need this #if statement for platform examples that enable the ICD management server without building the sample
// as an ICD. Since this is not spec compliant, we should remove this #if statement once we stop compiling the ICD management
// server in those examples.
#if CHIP_CONFIG_ENABLE_ICD_SERVER
Server::GetInstance().GetICDManager().ReleaseObserver(this);
#endif
DefaultServerCluster::Shutdown(shutdownType);
}
DataModel::ActionReturnStatus ICDManagementCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & aEncoder)
{
switch (request.path.mAttributeId)
{
case IcdManagement::Attributes::ClusterRevision::Id:
return aEncoder.Encode(kRevision);
case IcdManagement::Attributes::FeatureMap::Id:
return aEncoder.Encode(mICDConfigurationData.GetFeatureMap());
case IcdManagement::Attributes::IdleModeDuration::Id:
return aEncoder.Encode(mICDConfigurationData.GetIdleModeDuration().count());
case IcdManagement::Attributes::ActiveModeDuration::Id:
return aEncoder.Encode(mICDConfigurationData.GetActiveModeDuration().count());
case IcdManagement::Attributes::ActiveModeThreshold::Id:
return aEncoder.Encode(mICDConfigurationData.GetActiveModeThreshold().count());
#if CHIP_CONFIG_ENABLE_ICD_LIT
case IcdManagement::Attributes::OperatingMode::Id:
return ReadOperatingMode(aEncoder);
#endif // CHIP_CONFIG_ENABLE_ICD_LIT
case IcdManagement::Attributes::UserActiveModeTriggerHint::Id:
return aEncoder.Encode(mUserActiveModeTriggerBitmap);
case IcdManagement::Attributes::UserActiveModeTriggerInstruction::Id:
return aEncoder.Encode(CharSpan(mUserActiveModeTriggerInstruction, mUserActiveModeTriggerInstructionLength));
default:
return Protocols::InteractionModel::Status::UnsupportedAttribute;
}
}
CHIP_ERROR ICDManagementCluster::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder attributeListBuilder(builder);
BitFlags<IcdManagement::Feature> featureMap = mICDConfigurationData.GetFeatureMap();
const bool hasUAT = featureMap.Has(IcdManagement::Feature::kUserActiveModeTrigger);
const bool hasLIT = featureMap.Has(IcdManagement::Feature::kLongIdleTimeSupport);
const bool hasUATInstruction = mOptionalAttributeSet.IsSet(IcdManagement::Attributes::UserActiveModeTriggerInstruction::Id);
const AttributeListBuilder::OptionalAttributeEntry optionalEntries[] = {
{ hasUAT, IcdManagement::Attributes::UserActiveModeTriggerHint::kMetadataEntry },
{ hasUATInstruction, IcdManagement::Attributes::UserActiveModeTriggerInstruction::kMetadataEntry },
{ hasLIT, IcdManagement::Attributes::OperatingMode::kMetadataEntry },
};
return attributeListBuilder.Append(Span(IcdManagement::Attributes::kMandatoryMetadata), Span(optionalEntries));
}
std::optional<DataModel::ActionReturnStatus> ICDManagementCluster::InvokeCommand(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case IcdManagement::Commands::StayActiveRequest::Id: {
// TODO(#32321): Remove #if after issue is resolved
// Note: We only need this #if statement for platform examples that enable the ICD management server without building the sample
// as an ICD. Since this is not spec compliant, we should remove this #if statement once we stop compiling the ICD management
// server in those examples.
#if CHIP_CONFIG_ENABLE_ICD_SERVER
Commands::StayActiveRequest::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments));
Commands::StayActiveResponse::Type response;
response.promisedActiveDuration = Server::GetInstance().GetICDManager().StayActiveRequest(commandData.stayActiveDuration);
handler->AddResponse(request.path, response);
return std::nullopt;
#else
return Status::UnsupportedCommand;
#endif // CHIP_CONFIG_ENABLE_ICD_SERVER
}
default:
return Status::UnsupportedCommand;
}
}
CHIP_ERROR ICDManagementCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
if (mICDConfigurationData.GetFeatureMap().Has(Feature::kLongIdleTimeSupport) ||
mEnabledCommands.Has(OptionalCommands::kStayActive))
{
static constexpr DataModel::AcceptedCommandEntry kStayActiveCommand[] = {
Commands::StayActiveRequest::kMetadataEntry,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kStayActiveCommand));
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ICDManagementCluster::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder)
{
if (mICDConfigurationData.GetFeatureMap().Has(Feature::kLongIdleTimeSupport) ||
mEnabledCommands.Has(OptionalCommands::kStayActive))
{
static constexpr CommandId kStayActiveResponse[] = {
Commands::StayActiveResponse::Id,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kStayActiveResponse));
}
return CHIP_NO_ERROR;
}
void ICDManagementCluster::OnICDModeChange()
{
// Notify attribute change for OperatingMode attribute
NotifyAttributeChanged(IcdManagement::Attributes::OperatingMode::Id);
}
#if CHIP_CONFIG_ENABLE_ICD_LIT
CHIP_ERROR ICDManagementCluster::ReadOperatingMode(AttributeValueEncoder & encoder)
{
return mICDConfigurationData.GetICDMode() == ICDConfigurationData::ICDMode::SIT
? encoder.Encode(IcdManagement::OperatingModeEnum::kSit)
: encoder.Encode(IcdManagement::OperatingModeEnum::kLit);
}
#endif // CHIP_CONFIG_ENABLE_ICD_LIT
#if CHIP_CONFIG_ENABLE_ICD_CIP
ICDManagementClusterWithCIP::ICDManagementClusterWithCIP(
EndpointId endpointId, Crypto::SymmetricKeystore & symmetricKeystore, FabricTable & fabricTable,
ICDConfigurationData & icdConfigurationData, OptionalAttributeSet optionalAttributeSet,
BitMask<IcdManagement::OptionalCommands> enabledCommands,
BitMask<IcdManagement::UserActiveModeTriggerBitmap> userActiveModeTriggerBitmap, CharSpan userActiveModeTriggerInstruction) :
ICDManagementCluster(endpointId, symmetricKeystore, fabricTable, icdConfigurationData, optionalAttributeSet, enabledCommands,
userActiveModeTriggerBitmap, userActiveModeTriggerInstruction)
{}
CHIP_ERROR ICDManagementClusterWithCIP::Startup(ServerClusterContext & context)
{
ReturnErrorOnFailure(ICDManagementCluster::Startup(context));
mFabricDelegate.Init(context.storage, &mSymmetricKeystore, mICDConfigurationData);
return mFabricTable.AddFabricDelegate(&mFabricDelegate);
}
void ICDManagementClusterWithCIP::Shutdown(ClusterShutdownType shutdownType)
{
mFabricTable.RemoveFabricDelegate(&mFabricDelegate);
ICDManagementCluster::Shutdown(shutdownType);
}
DataModel::ActionReturnStatus ICDManagementClusterWithCIP::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & aEncoder)
{
switch (request.path.mAttributeId)
{
case IcdManagement::Attributes::RegisteredClients::Id:
return ReadRegisteredClients(aEncoder);
case IcdManagement::Attributes::ICDCounter::Id:
return aEncoder.Encode(mICDConfigurationData.GetICDCounter().GetValue());
case IcdManagement::Attributes::ClientsSupportedPerFabric::Id:
return aEncoder.Encode(mICDConfigurationData.GetClientsSupportedPerFabric());
case IcdManagement::Attributes::MaximumCheckInBackOff::Id:
return aEncoder.Encode(mICDConfigurationData.GetMaximumCheckInBackoff().count());
default:
// Delegate to base class for all other attributes
return ICDManagementCluster::ReadAttribute(request, aEncoder);
}
}
CHIP_ERROR ICDManagementClusterWithCIP::Attributes(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
// First get the base class attributes
ReturnErrorOnFailure(ICDManagementCluster::Attributes(path, builder));
// Add CIP-specific attributes
BitFlags<IcdManagement::Feature> featureMap = mICDConfigurationData.GetFeatureMap();
const bool hasCIP = featureMap.Has(Feature::kCheckInProtocolSupport);
if (hasCIP)
{
static constexpr DataModel::AttributeEntry kCIPAttributes[] = {
Attributes::RegisteredClients::kMetadataEntry,
Attributes::ICDCounter::kMetadataEntry,
Attributes::ClientsSupportedPerFabric::kMetadataEntry,
Attributes::MaximumCheckInBackOff::kMetadataEntry,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kCIPAttributes));
}
return CHIP_NO_ERROR;
}
std::optional<DataModel::ActionReturnStatus> ICDManagementClusterWithCIP::InvokeCommand(const DataModel::InvokeRequest & request,
TLV::TLVReader & input_arguments,
CommandHandler * handler)
{
switch (request.path.mCommandId)
{
case IcdManagement::Commands::RegisterClient::Id: {
Commands::RegisterClient::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments, handler->GetAccessingFabricIndex()));
uint32_t icdCounter = 0;
Status status = RegisterClient(handler, request.path, commandData, icdCounter);
if (status == Status::Success)
{
Commands::RegisterClientResponse::Type response;
response.ICDCounter = icdCounter;
handler->AddResponse(request.path, response);
}
else
{
handler->AddStatus(request.path, status);
}
break;
}
case IcdManagement::Commands::UnregisterClient::Id: {
Commands::UnregisterClient::DecodableType commandData;
ReturnErrorOnFailure(commandData.Decode(input_arguments, handler->GetAccessingFabricIndex()));
Status status = UnregisterClient(handler, request.path, commandData);
handler->AddStatus(request.path, status);
break;
}
default:
// Delegate to base class for all other commands
return ICDManagementCluster::InvokeCommand(request, input_arguments, handler);
}
return std::nullopt;
}
CHIP_ERROR ICDManagementClusterWithCIP::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
ReturnErrorOnFailure(ICDManagementCluster::AcceptedCommands(path, builder));
BitFlags<IcdManagement::Feature> featureMap = mICDConfigurationData.GetFeatureMap();
const bool hasCIP = featureMap.Has(Feature::kCheckInProtocolSupport);
if (hasCIP)
{
static constexpr DataModel::AcceptedCommandEntry kCIPCommands[] = {
Commands::RegisterClient::kMetadataEntry,
Commands::UnregisterClient::kMetadataEntry,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kCIPCommands));
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ICDManagementClusterWithCIP::GeneratedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<CommandId> & builder)
{
ReturnErrorOnFailure(ICDManagementCluster::GeneratedCommands(path, builder));
BitFlags<IcdManagement::Feature> featureMap = mICDConfigurationData.GetFeatureMap();
const bool hasCIP = featureMap.Has(Feature::kCheckInProtocolSupport);
if (hasCIP)
{
static constexpr CommandId kCIPResponses[] = {
Commands::RegisterClientResponse::Id,
};
ReturnErrorOnFailure(builder.ReferenceExisting(kCIPResponses));
}
return CHIP_NO_ERROR;
}
CHIP_ERROR ICDManagementClusterWithCIP::ReadRegisteredClients(AttributeValueEncoder & encoder)
{
uint16_t supported_clients = mICDConfigurationData.GetClientsSupportedPerFabric();
PersistentStorageDelegate & storage = mContext->storage;
Crypto::SymmetricKeystore & symmetricKeystore = mSymmetricKeystore;
const FabricTable & fabricTable = mFabricTable;
return encoder.EncodeList(
[supported_clients, &storage, &symmetricKeystore, &fabricTable](const auto & subEncoder) -> CHIP_ERROR {
ICDMonitoringEntry e(&symmetricKeystore);
for (const auto & fabricInfo : fabricTable)
{
ICDMonitoringTable table(storage, fabricInfo.GetFabricIndex(), supported_clients, &symmetricKeystore);
for (uint16_t i = 0; i < table.Limit(); ++i)
{
CHIP_ERROR err = table.Get(i, e);
if (CHIP_ERROR_NOT_FOUND == err)
{
// No more entries in the table
break;
}
ReturnErrorOnFailure(err);
Structs::MonitoringRegistrationStruct::Type s{ .checkInNodeID = e.checkInNodeID,
.monitoredSubject = e.monitoredSubject,
.clientType = e.clientType,
.fabricIndex = e.fabricIndex };
ReturnErrorOnFailure(subEncoder.Encode(s));
}
}
return CHIP_NO_ERROR;
});
}
void ICDManagementClusterWithCIP::TriggerICDMTableUpdatedEvent()
{
ICDNotifier::GetInstance().NotifyICDManagementEvent(ICDListener::ICDManagementEvents::kTableUpdated);
}
Status ICDManagementClusterWithCIP::RegisterClient(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::RegisterClient::DecodableType & commandData,
uint32_t & icdCounter)
{
FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex();
NodeId nodeId = commandData.checkInNodeID;
uint64_t monitoredSubject = commandData.monitoredSubject;
ClientTypeEnum clientType = commandData.clientType;
ByteSpan key = commandData.key;
Optional<ByteSpan> verificationKey = commandData.verificationKey;
bool isClientAdmin = false;
// Check if ClientType is valid
VerifyOrReturnError(clientType != ClientTypeEnum::kUnknownEnumValue, Status::ConstraintError);
// Check if client is admin
VerifyOrReturnError(CHIP_NO_ERROR == CheckAdmin(commandObj, commandPath, isClientAdmin), Status::Failure);
bool isFirstEntryForFabric = false;
ICDMonitoringTable table(mContext->storage, fabricIndex, mICDConfigurationData.GetClientsSupportedPerFabric(),
&mSymmetricKeystore);
// Get current entry, if exists
ICDMonitoringEntry entry(&mSymmetricKeystore);
CHIP_ERROR err = table.Find(nodeId, entry);
if (CHIP_NO_ERROR == err)
{
// Existing entry: Validate Key if, and only if, the ISD does NOT have administrator permissions
if (!isClientAdmin)
{
VerifyOrReturnError(verificationKey.HasValue(), Status::Failure);
VerifyOrReturnError(entry.IsKeyEquivalent(verificationKey.Value()), Status::Failure);
}
}
else if (CHIP_ERROR_NOT_FOUND == err)
{
// New entry
VerifyOrReturnError(entry.index < table.Limit(), Status::ResourceExhausted);
// Check if it's going to be the first entry for fabric
isFirstEntryForFabric = table.IsEmpty();
}
else
{
// Error
return Status::Failure;
}
// Save
entry.checkInNodeID = nodeId;
entry.monitoredSubject = monitoredSubject;
entry.clientType = clientType;
if (entry.keyHandleValid)
{
TEMPORARY_RETURN_IGNORED entry.DeleteKey();
}
err = entry.SetKey(key);
VerifyOrReturnError(CHIP_ERROR_INVALID_ARGUMENT != err, Status::ConstraintError);
VerifyOrReturnError(CHIP_NO_ERROR == err, Status::Failure);
err = table.Set(entry.index, entry);
// Delete key upon failure to prevent key storage leakage.
if (err != CHIP_NO_ERROR)
{
TEMPORARY_RETURN_IGNORED entry.DeleteKey();
}
VerifyOrReturnError(CHIP_ERROR_INVALID_ARGUMENT != err, Status::ConstraintError);
VerifyOrReturnError(CHIP_NO_ERROR == err, Status::Failure);
if (isFirstEntryForFabric)
{
// Notify subscribers that the first entry for the fabric was successfully added
TriggerICDMTableUpdatedEvent();
}
MarkRegisteredClientsListChanged();
icdCounter = mICDConfigurationData.GetICDCounter().GetValue();
return Status::Success;
}
Status ICDManagementClusterWithCIP::UnregisterClient(CommandHandler * commandObj, const ConcreteCommandPath & commandPath,
const Commands::UnregisterClient::DecodableType & commandData)
{
FabricIndex fabricIndex = commandObj->GetAccessingFabricIndex();
NodeId nodeId = commandData.checkInNodeID;
Optional<ByteSpan> verificationKey = commandData.verificationKey;
bool isClientAdmin = false;
// Check if client is admin
VerifyOrReturnError(CHIP_NO_ERROR == CheckAdmin(commandObj, commandPath, isClientAdmin), Status::Failure);
ICDMonitoringTable table(mContext->storage, fabricIndex, mICDConfigurationData.GetClientsSupportedPerFabric(),
&mSymmetricKeystore);
// Get current entry, if exists
ICDMonitoringEntry entry(&mSymmetricKeystore);
CHIP_ERROR err = table.Find(nodeId, entry);
VerifyOrReturnError(CHIP_ERROR_NOT_FOUND != err, Status::NotFound);
VerifyOrReturnError(CHIP_NO_ERROR == err, Status::Failure);
// Existing entry: Validate Key if, and only if, the ISD has NOT administrator permissions
if (!isClientAdmin)
{
VerifyOrReturnError(verificationKey.HasValue(), Status::Failure);
VerifyOrReturnError(entry.IsKeyEquivalent(verificationKey.Value()), Status::Failure);
}
err = table.Remove(entry.index);
VerifyOrReturnError(CHIP_NO_ERROR == err, Status::Failure);
if (table.IsEmpty())
{
TriggerICDMTableUpdatedEvent();
}
MarkRegisteredClientsListChanged();
return Status::Success;
}
void ICDManagementClusterWithCIP::MarkRegisteredClientsListChanged()
{
NotifyAttributeChanged(IcdManagement::Attributes::RegisteredClients::Id);
}
#endif // CHIP_CONFIG_ENABLE_ICD_CIP
} // namespace chip::app::Clusters