diff --git a/src/app/FailSafeContext.cpp b/src/app/FailSafeContext.cpp index 95a5b267f2aa5e..372ee323930d67 100644 --- a/src/app/FailSafeContext.cpp +++ b/src/app/FailSafeContext.cpp @@ -86,9 +86,12 @@ void FailSafeContext::ScheduleFailSafeCleanup(FabricIndex fabricIndex, bool addN SetFailSafeArmed(false); ChipDeviceEvent event{ .Type = DeviceEventType::kFailSafeTimerExpired, - .FailSafeTimerExpired = { .fabricIndex = fabricIndex, - .addNocCommandHasBeenInvoked = addNocCommandInvoked, - .updateNocCommandHasBeenInvoked = updateNocCommandInvoked } }; + .FailSafeTimerExpired = { + .fabricIndex = fabricIndex, + .addNocCommandHasBeenInvoked = addNocCommandInvoked, + .updateNocCommandHasBeenInvoked = updateNocCommandInvoked, + .updateTermsAndConditionsHasBeenInvoked = mUpdateTermsAndConditionsHasBeenInvoked, + } }; CHIP_ERROR status = PlatformMgr().PostEvent(&event); if (status != CHIP_NO_ERROR) diff --git a/src/app/FailSafeContext.h b/src/app/FailSafeContext.h index 48e11e0845395b..af177bd2a8d5fc 100644 --- a/src/app/FailSafeContext.h +++ b/src/app/FailSafeContext.h @@ -56,6 +56,7 @@ class FailSafeContext void SetUpdateNocCommandInvoked() { mUpdateNocCommandHasBeenInvoked = true; } void SetAddTrustedRootCertInvoked() { mAddTrustedRootCertHasBeenInvoked = true; } void SetCsrRequestForUpdateNoc(bool isForUpdateNoc) { mIsCsrRequestForUpdateNoc = isForUpdateNoc; } + void SetUpdateTermsAndConditionsHasBeenInvoked() { mUpdateTermsAndConditionsHasBeenInvoked = true; } /** * @brief @@ -91,6 +92,7 @@ class FailSafeContext bool UpdateNocCommandHasBeenInvoked() const { return mUpdateNocCommandHasBeenInvoked; } bool AddTrustedRootCertHasBeenInvoked() const { return mAddTrustedRootCertHasBeenInvoked; } bool IsCsrRequestForUpdateNoc() const { return mIsCsrRequestForUpdateNoc; } + bool UpdateTermsAndConditionsHasBeenInvoked() { return mUpdateTermsAndConditionsHasBeenInvoked; } FabricIndex GetFabricIndex() const { @@ -109,8 +111,9 @@ class FailSafeContext bool mUpdateNocCommandHasBeenInvoked = false; bool mAddTrustedRootCertHasBeenInvoked = false; // The fact of whether a CSR occurred at all is stored elsewhere. - bool mIsCsrRequestForUpdateNoc = false; - FabricIndex mFabricIndex = kUndefinedFabricIndex; + bool mIsCsrRequestForUpdateNoc = false; + FabricIndex mFabricIndex = kUndefinedFabricIndex; + bool mUpdateTermsAndConditionsHasBeenInvoked = false; /** * @brief @@ -140,11 +143,12 @@ class FailSafeContext { SetFailSafeArmed(false); - mAddNocCommandHasBeenInvoked = false; - mUpdateNocCommandHasBeenInvoked = false; - mAddTrustedRootCertHasBeenInvoked = false; - mFailSafeBusy = false; - mIsCsrRequestForUpdateNoc = false; + mAddNocCommandHasBeenInvoked = false; + mUpdateNocCommandHasBeenInvoked = false; + mAddTrustedRootCertHasBeenInvoked = false; + mFailSafeBusy = false; + mIsCsrRequestForUpdateNoc = false; + mUpdateTermsAndConditionsHasBeenInvoked = false; } void FailSafeTimerExpired(); diff --git a/src/app/chip_data_model.cmake b/src/app/chip_data_model.cmake index 11db6d4ec6c97b..5cfe2a2c040cbf 100644 --- a/src/app/chip_data_model.cmake +++ b/src/app/chip_data_model.cmake @@ -84,8 +84,9 @@ function(chip_configure_data_model APP_TARGET) # CMAKE data model auto-includes the server side implementation target_sources(${APP_TARGET} ${SCOPE} ${CHIP_APP_BASE_DIR}/server/AclStorage.cpp - ${CHIP_APP_BASE_DIR}/server/DefaultAclStorage.cpp ${CHIP_APP_BASE_DIR}/server/CommissioningWindowManager.cpp + ${CHIP_APP_BASE_DIR}/server/DefaultAclStorage.cpp + ${CHIP_APP_BASE_DIR}/server/DefaultTermsAndConditionsProvider.cpp ${CHIP_APP_BASE_DIR}/server/Dnssd.cpp ${CHIP_APP_BASE_DIR}/server/EchoHandler.cpp ${CHIP_APP_BASE_DIR}/server/OnboardingCodesUtil.cpp diff --git a/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp b/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp index 4bf97face53740..d2070c0af7780a 100644 --- a/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp +++ b/src/app/clusters/general-commissioning-server/general-commissioning-server.cpp @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2021 Project CHIP Authors + * Copyright (c) 2021-2024 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. @@ -28,7 +28,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -95,6 +97,95 @@ CHIP_ERROR GeneralCommissioningAttrAccess::Read(const ConcreteReadAttributePath case SupportsConcurrentConnection::Id: { return ReadSupportsConcurrentConnection(aEncoder); } +#if CHIP_CONFIG_TC_REQUIRED + case TCAcceptedVersion::Id: { + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + Optional outTermsAndConditions; + + if (nullptr == termsAndConditionsProvider) + { + return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; + } + + CHIP_ERROR err = termsAndConditionsProvider->GetAcceptance(outTermsAndConditions); + if (CHIP_NO_ERROR != err) + { + return CHIP_ERROR_INTERNAL; + } + + return aEncoder.Encode(outTermsAndConditions.ValueOr((TermsAndConditions){ .value = 0, .version = 0 }).version); + } + case TCMinRequiredVersion::Id: { + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + Optional outTermsAndConditions; + + if (nullptr == termsAndConditionsProvider) + { + return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; + } + + CHIP_ERROR err = termsAndConditionsProvider->GetRequirements(outTermsAndConditions); + if (CHIP_NO_ERROR != err) + { + return CHIP_ERROR_INTERNAL; + } + + return aEncoder.Encode(outTermsAndConditions.ValueOr((TermsAndConditions){ .value = 0, .version = 0 }).version); + } + case TCAcknowledgements::Id: { + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + Optional outTermsAndConditions; + + if (nullptr == termsAndConditionsProvider) + { + return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; + } + + CHIP_ERROR err = termsAndConditionsProvider->GetAcceptance(outTermsAndConditions); + if (CHIP_NO_ERROR != err) + { + return CHIP_ERROR_INTERNAL; + } + + return aEncoder.Encode(outTermsAndConditions.ValueOr((TermsAndConditions){ .value = 0, .version = 0 }).value); + } + case TCAcknowledgementsRequired::Id: { + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + Optional outTermsAndConditions; + TermsAndConditionsState termsAndConditionsState; + + if (nullptr == termsAndConditionsProvider) + { + return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; + } + + CHIP_ERROR err = termsAndConditionsProvider->GetAcceptance(outTermsAndConditions); + if (CHIP_NO_ERROR != err) + { + return CHIP_ERROR_INTERNAL; + } + + err = termsAndConditionsProvider->CheckAcceptance(outTermsAndConditions, termsAndConditionsState); + if (CHIP_NO_ERROR != err) + { + return CHIP_ERROR_INTERNAL; + } + + bool setTermsAndConditionsCallRequiredBeforeCommissioningCompleteSuccess = + termsAndConditionsState != TermsAndConditionsState::OK; + return aEncoder.Encode(setTermsAndConditionsCallRequiredBeforeCommissioningCompleteSuccess); + } + case TCUpdateDeadline::Id: { + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + + if (nullptr == termsAndConditionsProvider) + { + return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; + } + + return aEncoder.EncodeNull(); + } +#endif // CHIP_CONFIG_TC_REQUIRED default: { break; } @@ -144,6 +235,70 @@ CHIP_ERROR GeneralCommissioningAttrAccess::ReadSupportsConcurrentConnection(Attr return aEncoder.Encode(supportsConcurrentConnection); } +#if CHIP_CONFIG_TC_REQUIRED +CommissioningErrorEnum CheckTermsAndConditionsAcknowledgementsState(TermsAndConditionsProvider * const termsAndConditionsProvider, + const Optional & acceptedTermsAndConditions) +{ + TermsAndConditionsState termsAndConditionsState; + + CHIP_ERROR err = termsAndConditionsProvider->CheckAcceptance(acceptedTermsAndConditions, termsAndConditionsState); + if (CHIP_NO_ERROR != err) + { + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kUnknownEnumValue; + } + + switch (termsAndConditionsState) + { + case TermsAndConditionsState::OK: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kOk; + case TermsAndConditionsState::TC_ACKNOWLEDGEMENTS_NOT_RECEIVED: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kTCAcknowledgementsNotReceived; + case TermsAndConditionsState::TC_MIN_VERSION_NOT_MET: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kTCMinVersionNotMet; + case TermsAndConditionsState::REQUIRED_TC_NOT_ACCEPTED: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kRequiredTCNotAccepted; + } + + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kOk; +} + +CommissioningErrorEnum CheckTermsAndConditionsAcknowledgements(TermsAndConditionsProvider * const termsAndConditionsProvider) +{ + Optional acceptedTermsAndConditions; + TermsAndConditionsState termsAndConditionsState; + + CHIP_ERROR err = termsAndConditionsProvider->GetAcceptance(acceptedTermsAndConditions); + if (err != CHIP_NO_ERROR) + { + ChipLogError(FailSafe, "GeneralCommissioning: Failed to get terms and conditions acceptance: %" CHIP_ERROR_FORMAT, + err.Format()); + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kTCAcknowledgementsNotReceived; + } + + err = termsAndConditionsProvider->CheckAcceptance(acceptedTermsAndConditions, termsAndConditionsState); + if (CHIP_NO_ERROR != err) + { + ChipLogError(FailSafe, "GeneralCommissioning: Failed to verify terms and conditions acceptance: %" CHIP_ERROR_FORMAT, + err.Format()); + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kRequiredTCNotAccepted; + } + + switch (termsAndConditionsState) + { + case TermsAndConditionsState::OK: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kOk; + case TermsAndConditionsState::TC_ACKNOWLEDGEMENTS_NOT_RECEIVED: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kTCAcknowledgementsNotReceived; + case TermsAndConditionsState::TC_MIN_VERSION_NOT_MET: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kTCMinVersionNotMet; + case TermsAndConditionsState::REQUIRED_TC_NOT_ACCEPTED: + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kRequiredTCNotAccepted; + } + + return chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum::kOk; +} +#endif // CHIP_CONFIG_TC_REQUIRED + } // anonymous namespace bool emberAfGeneralCommissioningClusterArmFailSafeCallback(app::CommandHandler * commandObj, @@ -218,60 +373,90 @@ bool emberAfGeneralCommissioningClusterCommissioningCompleteCallback( auto & failSafe = Server::GetInstance().GetFailSafeContext(); auto & fabricTable = Server::GetInstance().GetFabricTable(); + CHIP_ERROR err = CHIP_NO_ERROR; + ChipLogProgress(FailSafe, "GeneralCommissioning: Received CommissioningComplete"); Commands::CommissioningCompleteResponse::Type response; + + // Fail-safe must be armed if (!failSafe.IsFailSafeArmed()) { response.errorCode = CommissioningErrorEnum::kNoFailSafe; + commandObj->AddResponse(commandPath, response); + return true; } - else + + SessionHandle handle = commandObj->GetExchangeContext()->GetSessionHandle(); + + // Ensure it's a valid CASE session + if ((handle->GetSessionType() != Session::SessionType::kSecure) || + (handle->AsSecureSession()->GetSecureSessionType() != SecureSession::Type::kCASE) || + (!failSafe.MatchesFabricIndex(commandObj->GetAccessingFabricIndex()))) + { + response.errorCode = CommissioningErrorEnum::kInvalidAuthentication; + ChipLogError(FailSafe, "GeneralCommissioning: Got commissioning complete in invalid security context"); + commandObj->AddResponse(commandPath, response); + return true; + } + +#if CHIP_CONFIG_TC_REQUIRED + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + + // Ensure required terms and conditions have been accepted, then attempt to commit + if (nullptr != termsAndConditionsProvider) { - SessionHandle handle = commandObj->GetExchangeContext()->GetSessionHandle(); - // If not a CASE session, or the fabric does not match the fail-safe, - // error out. - if (handle->GetSessionType() != Session::SessionType::kSecure || - handle->AsSecureSession()->GetSecureSessionType() != SecureSession::Type::kCASE || - !failSafe.MatchesFabricIndex(commandObj->GetAccessingFabricIndex())) + response.errorCode = CheckTermsAndConditionsAcknowledgements(termsAndConditionsProvider); + if (CommissioningErrorEnum::kOk != response.errorCode) { - response.errorCode = CommissioningErrorEnum::kInvalidAuthentication; - ChipLogError(FailSafe, "GeneralCommissioning: Got commissioning complete in invalid security context"); + ChipLogError(FailSafe, "GeneralCommissioning: Terms and conditions not accepted"); + commandObj->AddResponse(commandPath, response); + return true; } - else + + if (failSafe.UpdateTermsAndConditionsHasBeenInvoked()) { - if (failSafe.NocCommandHasBeenInvoked()) + // Commit terms and conditions acceptance on commissioning complete + err = termsAndConditionsProvider->CommitAcceptance(); + if (err != CHIP_NO_ERROR) { - CHIP_ERROR err = fabricTable.CommitPendingFabricData(); - if (err != CHIP_NO_ERROR) - { - // No need to revert on error: CommitPendingFabricData always reverts if not fully successful. - ChipLogError(FailSafe, "GeneralCommissioning: Failed to commit pending fabric data: %" CHIP_ERROR_FORMAT, - err.Format()); - } - else - { - ChipLogProgress(FailSafe, "GeneralCommissioning: Successfully commited pending fabric data"); - } - CheckSuccess(err, Failure); + ChipLogError(FailSafe, "GeneralCommissioning: Failed to commit terms and conditions: %" CHIP_ERROR_FORMAT, + err.Format()); } + else + { + ChipLogProgress(FailSafe, "GeneralCommissioning: Successfully committed terms and conditions"); + } + CheckSuccess(err, Failure); + } + } +#endif // CHIP_CONFIG_TC_REQUIRED - /* - * Pass fabric of commissioner to DeviceControlSvr. - * This allows device to send messages back to commissioner. - * Once bindings are implemented, this may no longer be needed. - */ - failSafe.DisarmFailSafe(); - CheckSuccess( - devCtrl->PostCommissioningCompleteEvent(handle->AsSecureSession()->GetPeerNodeId(), handle->GetFabricIndex()), - Failure); - - Breadcrumb::Set(commandPath.mEndpointId, 0); - response.errorCode = CommissioningErrorEnum::kOk; + // Handle NOC commands + if (failSafe.NocCommandHasBeenInvoked()) + { + err = fabricTable.CommitPendingFabricData(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(FailSafe, "GeneralCommissioning: Failed to commit pending fabric data: %" CHIP_ERROR_FORMAT, err.Format()); + // CommitPendingFabricData reverts on error, no need to revert explicitly + } + else + { + ChipLogProgress(FailSafe, "GeneralCommissioning: Successfully committed pending fabric data"); } + CheckSuccess(err, Failure); } - commandObj->AddResponse(commandPath, response); + // Disarm the fail-safe and notify the DeviceControlServer + failSafe.DisarmFailSafe(); + err = devCtrl->PostCommissioningCompleteEvent(handle->AsSecureSession()->GetPeerNodeId(), handle->GetFabricIndex()); + CheckSuccess(err, Failure); + + Breadcrumb::Set(commandPath.mEndpointId, 0); + response.errorCode = CommissioningErrorEnum::kOk; + commandObj->AddResponse(commandPath, response); return true; } @@ -328,6 +513,43 @@ bool emberAfGeneralCommissioningClusterSetRegulatoryConfigCallback(app::CommandH return true; } +bool emberAfGeneralCommissioningClusterSetTCAcknowledgementsCallback( + chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, + const chip::app::Clusters::GeneralCommissioning::Commands::SetTCAcknowledgements::DecodableType & commandData) +{ +#if CHIP_CONFIG_TC_REQUIRED + MATTER_TRACE_SCOPE("SetTCAcknowledgements", "GeneralCommissioning"); + + auto & failSafeContext = Server::GetInstance().GetFailSafeContext(); + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + + Optional acceptedTermsAndConditions = Optional({ + .value = commandData.TCUserResponse, + .version = commandData.TCVersion, + }); + + Commands::SetTCAcknowledgementsResponse::Type response; + response.errorCode = CheckTermsAndConditionsAcknowledgementsState(termsAndConditionsProvider, acceptedTermsAndConditions); + + if (CommissioningErrorEnum::kOk == response.errorCode) + { + CheckSuccess(termsAndConditionsProvider->SetAcceptance(acceptedTermsAndConditions), Failure); + + if (failSafeContext.IsFailSafeArmed()) + { + failSafeContext.SetUpdateTermsAndConditionsHasBeenInvoked(); + } + else + { + CheckSuccess(termsAndConditionsProvider->CommitAcceptance(), Failure); + } + } + + commandObj->AddResponse(commandPath, response); +#endif // CHIP_CONFIG_TC_REQUIRED + return true; +} + namespace { void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg) { @@ -335,16 +557,59 @@ void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t { // Spec says to reset Breadcrumb attribute to 0. Breadcrumb::Set(0, 0); + + if (event->FailSafeTimerExpired.updateTermsAndConditionsHasBeenInvoked) + { +#if CHIP_CONFIG_TC_REQUIRED + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + + if (nullptr == termsAndConditionsProvider) + { + return; + } + + // Clear terms and conditions acceptance on failsafe timer expiration + termsAndConditionsProvider->RevertAcceptance(); +#endif // CHIP_CONFIG_TC_REQUIRED + } } } } // anonymous namespace +class GeneralCommissioningFabricTableDelegate : public chip::FabricTable::Delegate +{ +public: + // Gets called when a fabric is deleted + void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) override + { + // If the FabricIndex matches the last remaining entry in the Fabrics list, then the device SHALL delete all Matter + // related data on the node which was created since it was commissioned. + if (Server::GetInstance().GetFabricTable().FabricCount() == 0) + { + ChipLogProgress(Zcl, "general-commissioning-server: Last Fabric index 0x%x was removed", + static_cast(fabricIndex)); + +#if CHIP_CONFIG_TC_REQUIRED + TermsAndConditionsProvider * const termsAndConditionsProvider = Server::GetInstance().GetTermsAndConditionsProvider(); + + if (nullptr != termsAndConditionsProvider) + { + termsAndConditionsProvider->ResetAcceptance(); + } +#endif // CHIP_CONFIG_TC_REQUIRED + } + } +}; + void MatterGeneralCommissioningPluginServerInitCallback() { Breadcrumb::Set(0, 0); AttributeAccessInterfaceRegistry::Instance().Register(&gAttrAccess); DeviceLayer::PlatformMgrImpl().AddEventHandler(OnPlatformEventHandler); + + static GeneralCommissioningFabricTableDelegate generalCommissioningFabricTableDelegate; + Server::GetInstance().GetFabricTable().AddFabricDelegate(&generalCommissioningFabricTableDelegate); } namespace chip { diff --git a/src/app/server/BUILD.gn b/src/app/server/BUILD.gn index 4addf8eea938e3..ed5cd155d02e22 100644 --- a/src/app/server/BUILD.gn +++ b/src/app/server/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Project CHIP Authors +# Copyright (c) 2020-2024 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. @@ -37,6 +37,8 @@ static_library("server") { "CommissioningWindowManager.h", "DefaultAclStorage.cpp", "DefaultAclStorage.h", + "DefaultTermsAndConditionsProvider.cpp", + "DefaultTermsAndConditionsProvider.h", "Dnssd.cpp", "Dnssd.h", "EchoHandler.cpp", @@ -45,6 +47,7 @@ static_library("server") { "OnboardingCodesUtil.h", "Server.cpp", "Server.h", + "TermsAndConditionsProvider.h", ] public_configs = [ ":server_config" ] diff --git a/src/app/server/DefaultTermsAndConditionsProvider.cpp b/src/app/server/DefaultTermsAndConditionsProvider.cpp new file mode 100644 index 00000000000000..f7ae51234f11a6 --- /dev/null +++ b/src/app/server/DefaultTermsAndConditionsProvider.cpp @@ -0,0 +1,300 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 "DefaultTermsAndConditionsProvider.h" +#include "TermsAndConditionsProvider.h" + +#include +#include +#include +#include +#include +#include + +#define VerifyNoErrorOrReturnValue(expr, code, ...) VerifyOrReturnValue(CHIP_NO_ERROR == expr, code, ##__VA_ARGS__) +#define VerifyNoErrorOrReturnInternal(expr, ...) VerifyNoErrorOrReturnValue(expr, CHIP_ERROR_INTERNAL, ##__VA_ARGS__) +#define VerifyOrReturnInvalidArgument(expr, ...) VerifyOrReturnValue(expr, CHIP_ERROR_INVALID_ARGUMENT, ##__VA_ARGS__) +#define VerifyOrReturnUninitialized(expr, ...) VerifyOrReturnValue(expr, CHIP_ERROR_UNINITIALIZED, ##__VA_ARGS__) +#define VerifyOrReturnInternal(expr, ...) VerifyOrReturnValue(expr, CHIP_ERROR_INTERNAL, ##__VA_ARGS__) +#define VerifyNotNullOrReturnInvalidArgument(expr, ...) \ + VerifyOrReturnValue(nullptr != expr, CHIP_ERROR_INVALID_ARGUMENT, ##__VA_ARGS__) +#define VerifyNotNullOrReturnUninitialized(expr, ...) VerifyOrReturnValue(nullptr != expr, CHIP_ERROR_UNINITIALIZED, ##__VA_ARGS__) + +namespace { +constexpr chip::TLV::Tag kSerializationVersionTag = chip::TLV::ContextTag(1); +constexpr chip::TLV::Tag kAcceptedAcknowledgementsTag = chip::TLV::ContextTag(2); +constexpr chip::TLV::Tag kAcceptedAcknowledgementsVersionTag = chip::TLV::ContextTag(3); +constexpr uint8_t kSerializationVersion = 1; + +constexpr size_t kEstimatedTlvBufferSize = chip::TLV::EstimateStructOverhead(sizeof(uint8_t), // SerializationVersion + sizeof(uint16_t), // AcceptedAcknowledgements + sizeof(uint16_t) // AcceptedAcknowledgementsVersion + ) * + 4; // Extra space for rollback compatibility +} // namespace + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Init(PersistentStorageDelegate * const inPersistentStorageDelegate) +{ + VerifyNotNullOrReturnInvalidArgument(inPersistentStorageDelegate); + + mStorageDelegate = inPersistentStorageDelegate; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Delete() +{ + VerifyNotNullOrReturnUninitialized(mStorageDelegate); + + const chip::StorageKeyName kStorageKey = chip::DefaultStorageKeyAllocator::TermsAndConditionsAcceptance(); + VerifyNoErrorOrReturnInternal(mStorageDelegate->SyncDeleteKeyValue(kStorageKey.KeyName())); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Get(Optional & outTermsAndConditions) +{ + VerifyNotNullOrReturnUninitialized(mStorageDelegate); + + uint8_t serializationVersion = 0; + uint16_t acknowledgements = 0; + uint16_t acknowledgementsVersion = 0; + + chip::TLV::TLVReader tlvReader; + chip::TLV::TLVType tlvContainer; + + uint8_t buffer[kEstimatedTlvBufferSize] = { 0 }; + uint16_t bufferSize = sizeof(buffer); + + const chip::StorageKeyName kStorageKey = chip::DefaultStorageKeyAllocator::TermsAndConditionsAcceptance(); + CHIP_ERROR err = mStorageDelegate->SyncGetKeyValue(kStorageKey.KeyName(), &buffer, bufferSize); + if (CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err) + { + outTermsAndConditions.ClearValue(); + return CHIP_NO_ERROR; + } + + VerifyNoErrorOrReturnInternal(err); + + tlvReader.Init(buffer, bufferSize); + VerifyNoErrorOrReturnInternal(tlvReader.Next(chip::TLV::kTLVType_Structure, chip::TLV::AnonymousTag())); + VerifyNoErrorOrReturnInternal(tlvReader.EnterContainer(tlvContainer)); + VerifyNoErrorOrReturnInternal(tlvReader.Next(kSerializationVersionTag)); + VerifyNoErrorOrReturnInternal(tlvReader.Get(serializationVersion)); + VerifyNoErrorOrReturnInternal(tlvReader.Next(kAcceptedAcknowledgementsTag)); + VerifyNoErrorOrReturnInternal(tlvReader.Get(acknowledgements)); + VerifyNoErrorOrReturnInternal(tlvReader.Next(kAcceptedAcknowledgementsVersionTag)); + VerifyNoErrorOrReturnInternal(tlvReader.Get(acknowledgementsVersion)); + VerifyNoErrorOrReturnInternal(tlvReader.ExitContainer(tlvContainer)); + + // Only v1 serialization format is supported + VerifyOrReturnInternal(kSerializationVersion == serializationVersion); + + outTermsAndConditions = Optional(TermsAndConditions{ + .value = acknowledgements, + .version = acknowledgementsVersion, + }); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Set(const TermsAndConditions & inTermsAndConditions) +{ + uint8_t buffer[kEstimatedTlvBufferSize] = { 0 }; + chip::TLV::TLVWriter tlvWriter; + chip::TLV::TLVType tlvContainer; + + VerifyNotNullOrReturnUninitialized(mStorageDelegate); + + tlvWriter.Init(buffer, sizeof(buffer)); + VerifyNoErrorOrReturnInternal(tlvWriter.StartContainer(chip::TLV::AnonymousTag(), chip::TLV::kTLVType_Structure, tlvContainer)); + VerifyNoErrorOrReturnInternal(tlvWriter.Put(kSerializationVersionTag, kSerializationVersion)); + VerifyNoErrorOrReturnInternal(tlvWriter.Put(kAcceptedAcknowledgementsTag, inTermsAndConditions.value)); + VerifyNoErrorOrReturnInternal(tlvWriter.Put(kAcceptedAcknowledgementsVersionTag, inTermsAndConditions.version)); + VerifyNoErrorOrReturnInternal(tlvWriter.EndContainer(tlvContainer)); + VerifyNoErrorOrReturnInternal(tlvWriter.Finalize()); + + uint32_t lengthWritten = tlvWriter.GetLengthWritten(); + VerifyOrReturnInternal(CanCastTo(lengthWritten)); + VerifyOrReturnInternal(lengthWritten <= kEstimatedTlvBufferSize); + + const chip::StorageKeyName kStorageKey = chip::DefaultStorageKeyAllocator::TermsAndConditionsAcceptance(); + VerifyNoErrorOrReturnInternal( + mStorageDelegate->SyncSetKeyValue(kStorageKey.KeyName(), buffer, static_cast(lengthWritten))); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::Init( + TermsAndConditionsStorageDelegate * const inStorageDelegate, + const chip::Optional & inRequiredTermsAndConditions) +{ + VerifyNotNullOrReturnInvalidArgument(inStorageDelegate); + + mTermsAndConditionsStorageDelegate = inStorageDelegate; + mRequiredAcknowledgements = inRequiredTermsAndConditions; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::CheckAcceptance(const Optional & inTermsAndConditions, + TermsAndConditionsState & outState) const +{ + VerifyNotNullOrReturnInvalidArgument(mTermsAndConditionsStorageDelegate); + + // No validation checks required if no required terms and conditions + if (!mRequiredAcknowledgements.HasValue()) + { + ChipLogProgress(AppServer, "No terms and conditions required"); + outState = TermsAndConditionsState::OK; + return CHIP_NO_ERROR; + } + + // Validate if we have received any terms and conditions acceptance + if (!inTermsAndConditions.HasValue()) + { + ChipLogError(AppServer, "No terms and conditions have been accepted"); + outState = TermsAndConditionsState::TC_ACKNOWLEDGEMENTS_NOT_RECEIVED; + return CHIP_NO_ERROR; + } + + const TermsAndConditions requiredTermsAndConditions = mRequiredAcknowledgements.Value(); + const TermsAndConditions termsAndConditionsToCheck = inTermsAndConditions.Value(); + + // Validate the accepted version first... + if (requiredTermsAndConditions.version > termsAndConditionsToCheck.version) + { + ChipLogError(AppServer, "Minimum terms and conditions version, 0x%04x, has not been accepted", + requiredTermsAndConditions.version); + outState = TermsAndConditionsState::TC_MIN_VERSION_NOT_MET; + return CHIP_NO_ERROR; + } + + // Validate the accepted bits second... + if (requiredTermsAndConditions.value != (requiredTermsAndConditions.value & termsAndConditionsToCheck.value)) + { + ChipLogError(AppServer, "Required terms and conditions, 0x%04x, have not been accepted", requiredTermsAndConditions.value); + outState = TermsAndConditionsState::REQUIRED_TC_NOT_ACCEPTED; + return CHIP_NO_ERROR; + } + + // All validation check succeeded... + ChipLogProgress(AppServer, "Required terms and conditions, 0x%04x, have been accepted", requiredTermsAndConditions.value); + outState = TermsAndConditionsState::OK; + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::CommitAcceptance() +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + if (!mTemporalAcceptance.HasValue()) + { + ChipLogError(AppServer, "No terms and conditions to commit"); + return CHIP_NO_ERROR; + } + + CHIP_ERROR err = mTermsAndConditionsStorageDelegate->Set(mTemporalAcceptance.Value()); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Failed storage delegate Set(): %" CHIP_ERROR_FORMAT, err.Format()); + return CHIP_ERROR_INTERNAL; + } + + ChipLogProgress(AppServer, "Terms and conditions have been committed"); + mTemporalAcceptance.ClearValue(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::GetAcceptance(Optional & outTermsAndConditions) const +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + // Return the in-memory acceptance state + if (mTemporalAcceptance.HasValue()) + { + outTermsAndConditions = mTemporalAcceptance; + return CHIP_NO_ERROR; + } + + // Otherwise, try to get the persisted acceptance state + CHIP_ERROR err = mTermsAndConditionsStorageDelegate->Get(outTermsAndConditions); + if (CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err) + { + ChipLogError(AppServer, "No terms and conditions have been accepted"); + return CHIP_NO_ERROR; + } + + // If the storage delegate returns an error, other than "value not found" then there was an unexpected datastore failure + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Failed storage delegate Get(): %" CHIP_ERROR_FORMAT, err.Format()); + return CHIP_ERROR_INTERNAL; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::GetRequirements(Optional & outTermsAndConditions) const +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + outTermsAndConditions = mRequiredAcknowledgements; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::ResetAcceptance() +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + CHIP_ERROR err = mTermsAndConditionsStorageDelegate->Delete(); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Failed storage delegate Delete(): %" CHIP_ERROR_FORMAT, err.Format()); + return CHIP_ERROR_INTERNAL; + } + + return RevertAcceptance(); +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::RevertAcceptance() +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + mTemporalAcceptance.ClearValue(); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::SetAcceptance(const Optional & inTermsAndConditions) +{ + VerifyNotNullOrReturnUninitialized(mTermsAndConditionsStorageDelegate); + + TermsAndConditionsState termsAndConditionsState = TermsAndConditionsState::OK; + + CHIP_ERROR err = CheckAcceptance(inTermsAndConditions, termsAndConditionsState); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Failed to check acceptance state: %" CHIP_ERROR_FORMAT, err.Format()); + return CHIP_ERROR_INVALID_ARGUMENT; + } + + mTemporalAcceptance = inTermsAndConditions; + return CHIP_NO_ERROR; +} diff --git a/src/app/server/DefaultTermsAndConditionsProvider.h b/src/app/server/DefaultTermsAndConditionsProvider.h new file mode 100644 index 00000000000000..662b20279d5298 --- /dev/null +++ b/src/app/server/DefaultTermsAndConditionsProvider.h @@ -0,0 +1,150 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 "TermsAndConditionsProvider.h" + +#include + +#include +#include +#include + +namespace chip { +namespace app { + +/** + * @brief Abstract interface for storing and retrieving terms and conditions acceptance status. + * + * This class defines the methods required to interact with the underlying storage system + * for saving, retrieving, and deleting the user's acceptance of terms and conditions. + */ +class TermsAndConditionsStorageDelegate +{ +public: + virtual ~TermsAndConditionsStorageDelegate() = default; + + /** + * @brief Deletes the persisted terms and conditions acceptance status from storage. + * + * This method deletes the stored record of the user's acceptance of the terms and conditions, + * effectively resetting their acceptance status in the persistent storage. + * + * @retval CHIP_NO_ERROR if the record was successfully deleted. + * @retval CHIP_ERROR_INTERNAL if there was an error deleting the record from storage. + * @retval CHIP_ERROR_UNINITIALIZED if the storage delegate is not properly initialized. + */ + virtual CHIP_ERROR Delete() = 0; + + /** + * @brief Retrieves the persisted terms and conditions acceptance status from storage. + * + * This method attempts to retrieve the previously accepted terms and conditions from + * persistent storage. If no such record exists, it returns an empty `Optional`. + * + * @param[out] outTermsAndConditions The retrieved terms and conditions, if any exist. + * + * @retval CHIP_NO_ERROR if the terms were successfully retrieved. + * @retval CHIP_ERROR_INTERNAL if an error occurred while retrieving from persistent storage. + * @retval CHIP_ERROR_UNINITIALIZED if the storage delegate is not properly initialized. + */ + virtual CHIP_ERROR Get(Optional & outTermsAndConditions) = 0; + + /** + * @brief Persists the user's acceptance of the terms and conditions. + * + * This method stores the provided terms and conditions acceptance status in persistent + * storage, allowing the user's acceptance to be retrieved later. + * + * @param[in] inTermsAndConditions The terms and conditions to be saved. + * + * @retval CHIP_NO_ERROR if the terms were successfully stored. + * @retval CHIP_ERROR_INTERNAL if there was an error during the storage operation. + * @retval CHIP_ERROR_UNINITIALIZED if the storage delegate is not properly initialized. + */ + virtual CHIP_ERROR Set(const TermsAndConditions & inTermsAndConditions) = 0; +}; + +/** + * @brief Default implementation of the TermsAndConditionsStorageDelegate using a persistent storage backend. + * + * This class provides an implementation of the TermsAndConditionsStorageDelegate interface, storing + * and retrieving the user's terms and conditions acceptance from persistent storage. It requires a + * PersistentStorageDelegate to interface with the storage system. + */ +class DefaultTermsAndConditionsStorageDelegate : public TermsAndConditionsStorageDelegate +{ +public: + /** + * @brief Initializes the storage delegate with a persistent storage backend. + * + * This method initializes the storage delegate with the provided persistent storage delegate. + * The storage delegate must be initialized before performing any operations. + * + * @param[in] inPersistentStorageDelegate The storage backend used for saving and retrieving data. + * + * @retval CHIP_NO_ERROR if the storage delegate was successfully initialized. + * @retval CHIP_ERROR_INVALID_ARGUMENT if the provided storage delegate is null. + */ + CHIP_ERROR Init(PersistentStorageDelegate * const inPersistentStorageDelegate); + + CHIP_ERROR Delete() override; + + CHIP_ERROR Get(Optional & inTermsAndConditions) override; + + CHIP_ERROR Set(const TermsAndConditions & inTermsAndConditions) override; + +private: + PersistentStorageDelegate * mStorageDelegate = nullptr; +}; + +class DefaultTermsAndConditionsProvider : public TermsAndConditionsProvider +{ +public: + /** + * @brief Initializes the TermsAndConditionsProvider. + * + * @param[in] inStorageDelegate Storage delegate dependency. + */ + CHIP_ERROR Init(TermsAndConditionsStorageDelegate * const inStorageDelegate, + const chip::Optional & inRequiredTermsAndConditions); + + CHIP_ERROR CheckAcceptance(const Optional & inTermsAndConditions, + TermsAndConditionsState & outState) const override; + + CHIP_ERROR CommitAcceptance() override; + + CHIP_ERROR GetAcceptance(Optional & outTermsAndConditions) const override; + + CHIP_ERROR GetRequirements(Optional & outTermsAndConditions) const override; + + CHIP_ERROR ResetAcceptance() override; + + CHIP_ERROR RevertAcceptance() override; + + CHIP_ERROR SetAcceptance(const Optional & inTermsAndConditions) override; + +private: + TermsAndConditionsStorageDelegate * mTermsAndConditionsStorageDelegate; + Optional mTemporalAcceptance; + Optional mRequiredAcknowledgements; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server/Server.cpp b/src/app/server/Server.cpp index e4db190c507608..989dc37138e423 100644 --- a/src/app/server/Server.cpp +++ b/src/app/server/Server.cpp @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2021 Project CHIP Authors + * Copyright (c) 2021-2024 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. @@ -222,6 +222,8 @@ CHIP_ERROR Server::Init(const ServerInitParams & initParams) mReportScheduler = initParams.reportScheduler; + mTermsAndConditionsProvider = initParams.termsAndConditionsProvider; + mTestEventTriggerDelegate = initParams.testEventTriggerDelegate; if (mTestEventTriggerDelegate == nullptr) { diff --git a/src/app/server/Server.h b/src/app/server/Server.h index 2f61197fcee2dd..ca8b248e3b39fb 100644 --- a/src/app/server/Server.h +++ b/src/app/server/Server.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-2024 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. @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -188,6 +189,9 @@ struct ServerInitParams // If the ICD Check-In protocol use-case is supported and no strategy is provided, server will use the default strategy. app::ICDCheckInBackOffStrategy * icdCheckInBackOffStrategy = nullptr; #endif // CHIP_CONFIG_ENABLE_ICD_CIP + + // Optional. Terms and conditions provider to support enhanced setup flow feature. + app::TermsAndConditionsProvider * termsAndConditionsProvider = nullptr; }; /** @@ -309,6 +313,23 @@ struct CommonCaseDeviceServerInitParams : public ServerInitParams } #endif +#if CHIP_CONFIG_TC_REQUIRED + static app::DefaultTermsAndConditionsProvider sDefaultTermsAndConditionsProviderInstance; + static app::DefaultTermsAndConditionsStorageDelegate sDefaultTermsAndConditionsStorageDelegateInstance; + + Optional termsAndConditions = Optional((app::TermsAndConditions){ + .value = CHIP_CONFIG_TC_REQUIRED_ACKNOWLEDGEMENTS, + .version = CHIP_CONFIG_TC_REQUIRED_ACKNOWLEDGEMENTS_VERSION, + }); + + ReturnErrorOnFailure(sDefaultTermsAndConditionsStorageDelegateInstance.Init(this->persistentStorageDelegate)); + ReturnErrorOnFailure(sDefaultTermsAndConditionsProviderInstance.Init(&sDefaultTermsAndConditionsStorageDelegateInstance, + termsAndConditions)); + this->termsAndConditionsProvider = &sDefaultTermsAndConditionsProviderInstance; +#else + this->termsAndConditionsProvider = nullptr; +#endif + return CHIP_NO_ERROR; } @@ -411,6 +432,8 @@ class Server app::reporting::ReportScheduler * GetReportScheduler() { return mReportScheduler; } + app::TermsAndConditionsProvider * GetTermsAndConditionsProvider() { return mTermsAndConditionsProvider; } + #if CHIP_CONFIG_ENABLE_ICD_SERVER app::ICDManager & GetICDManager() { return mICDManager; } @@ -684,6 +707,7 @@ class Server GroupDataProviderListener mListener; ServerFabricDelegate mFabricDelegate; app::reporting::ReportScheduler * mReportScheduler; + app::TermsAndConditionsProvider * mTermsAndConditionsProvider; Access::AccessControl mAccessControl; app::AclStorage * mAclStorage; diff --git a/src/app/server/TermsAndConditionsProvider.h b/src/app/server/TermsAndConditionsProvider.h new file mode 100644 index 00000000000000..723b0f4d9c42d0 --- /dev/null +++ b/src/app/server/TermsAndConditionsProvider.h @@ -0,0 +1,156 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 + +#include +#include + +namespace chip { +namespace app { + +typedef struct sTermsAndConditions +{ + uint16_t value; + uint16_t version; +} TermsAndConditions; + +typedef enum eTermsAndConditionsState +{ + OK = 0, + TC_ACKNOWLEDGEMENTS_NOT_RECEIVED = 1, + TC_MIN_VERSION_NOT_MET = 2, + REQUIRED_TC_NOT_ACCEPTED = 3, +} TermsAndConditionsState; + +/** + * @brief Data access layer for handling the required terms and conditions and managing user acceptance status. + * + * This class provides methods to manage the acceptance of terms and conditions, including storing, retrieving, + * and verifying the acceptance status. It also supports temporary in-memory storage and persistent storage for + * accepted terms and conditions. + */ +class TermsAndConditionsProvider +{ +public: + virtual ~TermsAndConditionsProvider() = default; + + /** + * @brief Verifies the acceptance of the terms and conditions. + * + * This function checks whether the provided terms and conditions meet the required criteria without + * affecting any persistent or temporary acceptance states. The function validates both the version + * and content of the terms and conditions against the required values. + * + * @param[in] inTermsAndConditions The terms and conditions to verify. + * @param[out] outState The result of the validation: whether the terms are accepted or + * why they are invalid (e.g., version mismatch or required terms not accepted). + * + * @retval CHIP_NO_ERROR if the terms are successfully validated. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR CheckAcceptance(const Optional & inTermsAndConditions, + TermsAndConditionsState & outState) const = 0; + + /** + * @brief Persists the acceptance of the terms and conditions. + * + * This method commits the in-memory acceptance status to persistent storage. It stores the acceptance + * status in a permanent location and clears the temporary in-memory acceptance state after committing. + * + * @retval CHIP_NO_ERROR if the terms were successfully persisted. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR CommitAcceptance() = 0; + + /** + * @brief Retrieves the current acceptance status of the terms and conditions. + * + * This method checks the temporary in-memory acceptance state first. If no in-memory state is found, + * it attempts to retrieve the acceptance status from persistent storage. If no terms have been accepted, + * it returns an empty `Optional`. + * + * @param[out] outTermsAndConditions The current accepted terms and conditions, if any. + * + * @retval CHIP_NO_ERROR if the terms were successfully retrieved or no terms were found. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR GetAcceptance(Optional & outTermsAndConditions) const = 0; + + /** + * @brief Retrieves the requirements for the terms and conditions. + * + * This method retrieves the required terms and conditions that must be accepted by the user. These + * requirements are set by the provider and used to validate the acceptance. + * + * @param[out] outTermsAndConditions The required terms and conditions. + * + * @retval CHIP_NO_ERROR if the required terms were successfully retrieved. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR GetRequirements(Optional & outTermsAndConditions) const = 0; + + /** + * @brief Resets the persisted acceptance status. + * + * This method deletes the persisted acceptance of the terms and conditions from storage, effectively + * resetting the stored acceptance status. Any in-memory temporary acceptance will also be cleared + * through this method. + * + * @retval CHIP_NO_ERROR if the terms were successfully reset. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR ResetAcceptance() = 0; + + /** + * @brief Clears the in-memory temporary acceptance status. + * + * This method clears any temporary acceptance of the terms and conditions that is held in-memory. It does + * not affect the persisted state stored in storage. + * + * @retval CHIP_NO_ERROR if the in-memory acceptance state was successfully cleared. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR RevertAcceptance() = 0; + + /** + * @brief Sets the temporary in-memory acceptance status of the terms and conditions. + * + * This method stores the provided terms and conditions acceptance status in-memory. It does not persist + * the acceptance status to storage. To persist the acceptance, call `CommitAcceptance()` after this method. + * + * @param[in] inTermsAndConditions The terms and conditions to be accepted temporarily. + * + * @retval CHIP_NO_ERROR if the terms were successfully stored in-memory. + * @retval CHIP_ERROR_INTERNAL if there was an error during the operation. + * @retval CHIP_ERROR_INVALID_ARGUMENT if the provided terms and conditions are invalid. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + */ + virtual CHIP_ERROR SetAcceptance(const Optional & inTermsAndConditions) = 0; +}; + +} // namespace app +} // namespace chip diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index 9ebed234e5b45e..0d32dd38745d61 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Project CHIP Authors +# Copyright (c) 2020-2024 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. @@ -213,6 +213,7 @@ chip_test_suite("tests") { "TestConcreteAttributePath.cpp", "TestDataModelSerialization.cpp", "TestDefaultOTARequestorStorage.cpp", + "TestDefaultTermsAndConditionsProvider.cpp", "TestDefaultThreadNetworkDirectoryStorage.cpp", "TestEcosystemInformationCluster.cpp", "TestEventLoggingNoUTCTime.cpp", @@ -254,6 +255,7 @@ chip_test_suite("tests") { "${chip_root}/src/app/data-model-provider/tests:encode-decode", "${chip_root}/src/app/icd/client:handler", "${chip_root}/src/app/icd/client:manager", + "${chip_root}/src/app/server", "${chip_root}/src/app/tests:helpers", "${chip_root}/src/app/util/mock:mock_codegen_data_model", "${chip_root}/src/app/util/mock:mock_ember", diff --git a/src/app/tests/TestDefaultTermsAndConditionsProvider.cpp b/src/app/tests/TestDefaultTermsAndConditionsProvider.cpp new file mode 100644 index 00000000000000..a3d9f6574226e9 --- /dev/null +++ b/src/app/tests/TestDefaultTermsAndConditionsProvider.cpp @@ -0,0 +1,477 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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/server/DefaultTermsAndConditionsProvider.h" +#include "app/server/TermsAndConditionsProvider.h" +#include "pw_unit_test/framework.h" + +#include +#include +#include +#include + +class TestTermsAndConditionsStorageDelegate : public chip::app::TermsAndConditionsStorageDelegate +{ +public: + TestTermsAndConditionsStorageDelegate(chip::Optional & initialTermsAndConditions) : + mTermsAndConditions(initialTermsAndConditions) + {} + + CHIP_ERROR Delete() + { + mTermsAndConditions.ClearValue(); + return CHIP_NO_ERROR; + } + + CHIP_ERROR Get(chip::Optional & outTermsAndConditions) + { + outTermsAndConditions = mTermsAndConditions; + return CHIP_NO_ERROR; + } + + CHIP_ERROR Set(const chip::app::TermsAndConditions & inTermsAndConditions) + { + mTermsAndConditions = chip::Optional(inTermsAndConditions); + return CHIP_NO_ERROR; + } + +private: + chip::Optional & mTermsAndConditions; +}; + +TEST(DefaultTermsAndConditionsProvider, TestInitSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); +} + +TEST(DefaultTermsAndConditionsProvider, TestNoRequirementsGetRequirementsSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional(); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_FALSE(outTermsAndConditions.HasValue()); +} + +TEST(DefaultTermsAndConditionsProvider, TestNeverAcceptanceGetAcceptanceSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 0b1111'1111'1111'1111, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_FALSE(outTermsAndConditions.HasValue()); +} + +TEST(DefaultTermsAndConditionsProvider, TestTermsAcceptedPersistsSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional newTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + + err = defaultTermsAndConditionsProvider.SetAcceptance(newTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); + + err = defaultTermsAndConditionsProvider.CommitAcceptance(); + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); + + chip::app::DefaultTermsAndConditionsProvider anotherTncProvider; + err = anotherTncProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + err = anotherTncProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); +} + +TEST(DefaultTermsAndConditionsProvider, TestTermsRequiredGetRequirementsSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetRequirements(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); +} + +TEST(DefaultTermsAndConditionsProvider, TestSetAcceptanceGetAcceptanceSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(acceptedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetRequirements(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceGetAcceptanceSuccess) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(acceptedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetRequirements(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().value); + EXPECT_EQ(1, outTermsAndConditions.Value().version); + + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outAcceptance2; + err = defaultTermsAndConditionsProvider.GetAcceptance(outAcceptance2); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_FALSE(outAcceptance2.HasValue()); +} + +TEST(DefaultTermsAndConditionsProvider, TestAcceptanceRequiredTermsMissingFailure) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(acceptedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outAcknowledgementTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetAcceptance(outAcknowledgementTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outAcknowledgementTermsAndConditions.Value().value); + EXPECT_EQ(1, outAcknowledgementTermsAndConditions.Value().version); + + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outRequiredTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetRequirements(outRequiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outRequiredTermsAndConditions.Value().value); + EXPECT_EQ(1, outRequiredTermsAndConditions.Value().version); +} + +TEST(DefaultTermsAndConditionsProvider, TestAcceptanceCommitCheckSetRevertCheckExpectCommitValue) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Initialize unit under test + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Set acceptance + chip::Optional acceptedTermsAndConditions = chip::Optional({ + .value = 0b1, + .version = 1, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(acceptedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Commit value + err = defaultTermsAndConditionsProvider.CommitAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Check commit value + chip::Optional outTermsAndConditions; + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_TRUE(outTermsAndConditions.HasValue()); + EXPECT_EQ(outTermsAndConditions.Value().value, acceptedTermsAndConditions.Value().value); + EXPECT_EQ(outTermsAndConditions.Value().version, acceptedTermsAndConditions.Value().version); + + // Set updated value + chip::Optional updatedTermsAndConditions = chip::Optional({ + .value = 0b11, + .version = 2, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(updatedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Check updated value + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_TRUE(outTermsAndConditions.HasValue()); + EXPECT_EQ(outTermsAndConditions.Value().value, updatedTermsAndConditions.Value().value); + EXPECT_EQ(outTermsAndConditions.Value().version, updatedTermsAndConditions.Value().version); + + // Revert updated value + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Check committed value + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_TRUE(outTermsAndConditions.HasValue()); + EXPECT_EQ(outTermsAndConditions.Value().value, acceptedTermsAndConditions.Value().value); + EXPECT_EQ(outTermsAndConditions.Value().version, acceptedTermsAndConditions.Value().version); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceWhileMissing) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + + chip::Optional outTermsAndConditions; + + err = defaultTermsAndConditionsStorageDelegate.Init(&testPersistentStorageDelegate); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Initialize unit under test [No conditions previously accepted] + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // [Fail-safe started] No conditions set during the fail-safe. No commit. + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_FALSE(outTermsAndConditions.HasValue()); + + // [Fail-safe expires] Revert is called. + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // [New fail safe started (to retry the commissioning operations)] Confirm acceptance returns previous values (empty) + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_FALSE(outTermsAndConditions.HasValue()); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceWhenPreviouslyAccepted) +{ + CHIP_ERROR err; + + // Initialize unit under test [Conditions previously accepted] + chip::Optional initialTermsAndConditions = chip::Optional({ + .value = 1, + .version = 1, + }); + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 0b11, + .version = 2, + }); + TestTermsAndConditionsStorageDelegate testTermsAndConditionsStorageDelegate(initialTermsAndConditions); + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + err = defaultTermsAndConditionsProvider.Init(&testTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // [Fail-safe started] No conditions set during the fail-safe. No commit. + + // [Fail-safe expires] Revert is called. + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + + // [New fail safe started (to retry the commissioning operations)] Confirm acceptance returns previous values (accepted) + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_TRUE(outTermsAndConditions.HasValue()); + EXPECT_EQ(outTermsAndConditions.Value().value, 1); + EXPECT_EQ(outTermsAndConditions.Value().version, 1); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceWhenPreviouslyAcceptedThenUpdatedUnderFailsafe) +{ + CHIP_ERROR err; + + // Initialize unit under test dependency + chip::Optional initiallyAcceptedTermsAndConditions = + chip::Optional({ + .value = 1, + .version = 1, + }); + TestTermsAndConditionsStorageDelegate testTermsAndConditionsStorageDelegate(initiallyAcceptedTermsAndConditions); + + // Initialize unit under test [Conditions previously accepted] + chip::Optional requiredTermsAndConditions = chip::Optional({ + .value = 0b11, + .version = 2, + }); + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + err = defaultTermsAndConditionsProvider.Init(&testTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // [Fail-safe started] Acceptance updated. + chip::Optional updatedAcceptedTermsAndConditions = + chip::Optional({ + .value = 0b111, + .version = 3, + }); + err = defaultTermsAndConditionsProvider.SetAcceptance(updatedAcceptedTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // [Fail-safe expires] Revert is called. + err = defaultTermsAndConditionsProvider.RevertAcceptance(); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional outTermsAndConditions; + + // [New fail safe started (to retry the commissioning operations)] Confirm acceptance returns previous values (accepted) + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_TRUE(outTermsAndConditions.HasValue()); + EXPECT_EQ(outTermsAndConditions.Value().value, initiallyAcceptedTermsAndConditions.Value().value); + EXPECT_EQ(outTermsAndConditions.Value().version, initiallyAcceptedTermsAndConditions.Value().version); +} diff --git a/src/include/platform/CHIPDeviceEvent.h b/src/include/platform/CHIPDeviceEvent.h index 09f4c46b652920..9618d93f5faa05 100644 --- a/src/include/platform/CHIPDeviceEvent.h +++ b/src/include/platform/CHIPDeviceEvent.h @@ -534,6 +534,7 @@ struct ChipDeviceEvent final FabricIndex fabricIndex; bool addNocCommandHasBeenInvoked; bool updateNocCommandHasBeenInvoked; + bool updateTermsAndConditionsHasBeenInvoked; } FailSafeTimerExpired; struct diff --git a/src/lib/core/CHIPConfig.h b/src/lib/core/CHIPConfig.h index 382c63497da51d..5574decbd1c94f 100644 --- a/src/lib/core/CHIPConfig.h +++ b/src/lib/core/CHIPConfig.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2020-2022 Project CHIP Authors + * Copyright (c) 2020-2024 Project CHIP Authors * Copyright (c) 2019 Google LLC. * Copyright (c) 2013-2018 Nest Labs, Inc. * @@ -1845,6 +1845,24 @@ extern const char CHIP_NON_PRODUCTION_MARKER[]; #define CHIP_CONFIG_MAX_BDX_LOG_TRANSFERS 5 #endif // CHIP_CONFIG_MAX_BDX_LOG_TRANSFERS +/** + * @file + * Configuration settings for Terms and Conditions (TC) acknowledgements during device commissioning. + */ + +/** + * @def CHIP_CONFIG_TC_REQUIRED + * + * @brief Indicates whether terms and conditions are required during commissioning. + * + * This macro defines whether the device commissioning process requires the user to acknowledge terms and conditions. + * - 1: Terms and conditions are required. + * - 0: Terms and conditions are not required. + */ +#ifndef CHIP_CONFIG_TC_REQUIRED +#define CHIP_CONFIG_TC_REQUIRED 0 +#endif + /** * @} */ diff --git a/src/lib/support/DefaultStorageKeyAllocator.h b/src/lib/support/DefaultStorageKeyAllocator.h index 9ed8a2f56cfd77..b0de78d085e48d 100644 --- a/src/lib/support/DefaultStorageKeyAllocator.h +++ b/src/lib/support/DefaultStorageKeyAllocator.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2021 Project CHIP Authors + * Copyright (c) 2021-2024 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. @@ -256,6 +256,10 @@ class DefaultStorageKeyAllocator // when new fabric is created, this list needs to be updated, // when client init DefaultICDClientStorage, this table needs to be loaded. static StorageKeyName ICDFabricList() { return StorageKeyName::FromConst("g/icdfl"); } + + // Terms and Conditions Acceptance Key + // Stores the terms and conditions acceptance including terms and conditions revision, TLV encoded + static StorageKeyName TermsAndConditionsAcceptance() { return StorageKeyName::FromConst("g/tc"); } }; } // namespace chip