diff --git a/scripts/build/build/targets.py b/scripts/build/build/targets.py index 0bb25ef9a06b75..d7935e95ffeaaf 100755 --- a/scripts/build/build/targets.py +++ b/scripts/build/build/targets.py @@ -197,6 +197,7 @@ def BuildHostTarget(): target.AppendModifier('disable-dnssd-tests', enable_dnssd_tests=False).OnlyIfRe('-tests') target.AppendModifier('chip-casting-simplified', chip_casting_simplified=True).OnlyIfRe('-tv-casting-app') target.AppendModifier('googletest', use_googletest=True).OnlyIfRe('-tests') + target.AppendModifier('terms-and-conditions', terms_and_conditions_required=True) return target diff --git a/scripts/build/builders/host.py b/scripts/build/builders/host.py index 7fe5823f37b7a8..fd84901a5adaba 100644 --- a/scripts/build/builders/host.py +++ b/scripts/build/builders/host.py @@ -336,6 +336,7 @@ def __init__(self, root, runner, app: HostApp, board=HostBoard.NATIVE, chip_casting_simplified: Optional[bool] = None, disable_shell=False, use_googletest=False, + terms_and_conditions_required: Optional[bool] = None, ): super(HostBuilder, self).__init__( root=os.path.join(root, 'examples', app.ExamplePath()), @@ -459,6 +460,12 @@ def __init__(self, root, runner, app: HostApp, board=HostBoard.NATIVE, if chip_casting_simplified is not None: self.extra_gn_options.append(f'chip_casting_simplified={str(chip_casting_simplified).lower()}') + if terms_and_conditions_required is not None: + if terms_and_conditions_required: + self.extra_gn_options.append('chip_terms_and_conditions_required=true') + else: + self.extra_gn_options.append('chip_terms_and_conditions_required=false') + if self.board == HostBoard.ARM64: if not use_clang: raise Exception("Cross compile only supported using clang") diff --git a/scripts/build/testdata/all_targets_linux_x64.txt b/scripts/build/testdata/all_targets_linux_x64.txt index f3330ffc3fe090..05fec168c491c6 100644 --- a/scripts/build/testdata/all_targets_linux_x64.txt +++ b/scripts/build/testdata/all_targets_linux_x64.txt @@ -9,7 +9,7 @@ efr32-{brd2704b,brd4316a,brd4317a,brd4318a,brd4319a,brd4186a,brd4187a,brd2601b,b esp32-{m5stack,c3devkit,devkitc,qemu}-{all-clusters,all-clusters-minimal,energy-management,ota-provider,ota-requestor,shell,light,lock,bridge,temperature-measurement,ota-requestor,tests}[-rpc][-ipv6only][-tracing] genio-lighting-app linux-fake-tests[-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-pw-fuzztest][-coverage][-dmalloc][-clang] -linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,kotlin-matter-controller,minmdns,light,light-data-model-no-unique-id,lock,shell,ota-provider,ota-requestor,simulated-app1,simulated-app2,python-bindings,tv-app,tv-casting-app,bridge,fabric-admin,fabric-bridge,fabric-sync,tests,chip-cert,address-resolve-tool,contact-sensor,dishwasher,microwave-oven,refrigerator,rvc,air-purifier,lit-icd,air-quality-sensor,network-manager,energy-management,water-leak-detector}[-nodeps][-nlfaultinject][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-no-shell][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-pw-fuzztest][-coverage][-dmalloc][-clang][-test][-rpc][-with-ui][-evse-test-event][-enable-dnssd-tests][-disable-dnssd-tests][-chip-casting-simplified][-googletest] +linux-{x64,arm64}-{rpc-console,all-clusters,all-clusters-minimal,chip-tool,thermostat,java-matter-controller,kotlin-matter-controller,minmdns,light,light-data-model-no-unique-id,lock,shell,ota-provider,ota-requestor,simulated-app1,simulated-app2,python-bindings,tv-app,tv-casting-app,bridge,fabric-admin,fabric-bridge,fabric-sync,tests,chip-cert,address-resolve-tool,contact-sensor,dishwasher,microwave-oven,refrigerator,rvc,air-purifier,lit-icd,air-quality-sensor,network-manager,energy-management,water-leak-detector}[-nodeps][-nlfaultinject][-platform-mdns][-minmdns-verbose][-libnl][-same-event-loop][-no-interactive][-ipv6only][-no-ble][-no-wifi][-no-thread][-no-shell][-mbedtls][-boringssl][-asan][-tsan][-ubsan][-libfuzzer][-ossfuzz][-pw-fuzztest][-coverage][-dmalloc][-clang][-test][-rpc][-with-ui][-evse-test-event][-enable-dnssd-tests][-disable-dnssd-tests][-chip-casting-simplified][-googletest][-terms-and-conditions] linux-x64-efr32-test-runner[-clang] imx-{chip-tool,lighting-app,thermostat,all-clusters-app,all-clusters-minimal-app,ota-provider-app}[-release] infineon-psoc6-{lock,light,all-clusters,all-clusters-minimal}[-ota][-updateimage][-trustm] diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index 14fcfc96e45543..fd286ff7c9c2d4 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -76,6 +76,7 @@ buildconfig_header("app_buildconfig") { "CHIP_DEVICE_CONFIG_DYNAMIC_SERVER=${chip_build_controller_dynamic_server}", "CHIP_CONFIG_ENABLE_BUSY_HANDLING_FOR_OPERATIONAL_SESSION_SETUP=${chip_enable_busy_handling_for_operational_session_setup}", "CHIP_CONFIG_DATA_MODEL_EXTRA_LOGGING=${chip_data_model_extra_logging}", + "CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED=${chip_terms_and_conditions_required}", ] visibility = [ ":app_config" ] 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 7c5ab0f82608b4..dcafc0055de3e9 100644 --- a/src/app/chip_data_model.cmake +++ b/src/app/chip_data_model.cmake @@ -81,8 +81,9 @@ function(chip_configure_data_model APP_TARGET) ${CHIP_APP_BASE_DIR}/SafeAttributePersistenceProvider.cpp ${CHIP_APP_BASE_DIR}/StorageDelegateWrapper.cpp ${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..f6fad1d8908243 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. @@ -21,20 +21,29 @@ ******************************************************************************* ******************************************************************************/ +#include "general-commissioning-server.h" + #include #include +#include #include #include #include +#include #include #include -#include -#include +#include #include #include #include #include #include +#include + +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED +#include //nogncheck +#include //nogncheck +#endif using namespace chip; using namespace chip::app; @@ -42,6 +51,7 @@ using namespace chip::app::Clusters; using namespace chip::app::Clusters::GeneralCommissioning; using namespace chip::app::Clusters::GeneralCommissioning::Attributes; using namespace chip::DeviceLayer; +using chip::app::Clusters::GeneralCommissioning::CommissioningErrorEnum; using Transport::SecureSession; using Transport::Session; @@ -95,6 +105,58 @@ CHIP_ERROR GeneralCommissioningAttrAccess::Read(const ConcreteReadAttributePath case SupportsConcurrentConnection::Id: { return ReadSupportsConcurrentConnection(aEncoder); } +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + case TCAcceptedVersion::Id: { + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + Optional outTermsAndConditions; + + VerifyOrReturnError(nullptr != tcProvider, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + ReturnErrorOnFailure(tcProvider->GetAcceptance(outTermsAndConditions)); + + return aEncoder.Encode(outTermsAndConditions.ValueOr(TermsAndConditions(0, 0)).GetVersion()); + } + case TCMinRequiredVersion::Id: { + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + Optional outTermsAndConditions; + + VerifyOrReturnError(nullptr != tcProvider, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + ReturnErrorOnFailure(tcProvider->GetRequirements(outTermsAndConditions)); + + return aEncoder.Encode(outTermsAndConditions.ValueOr(TermsAndConditions(0, 0)).GetVersion()); + } + case TCAcknowledgements::Id: { + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + Optional outTermsAndConditions; + + VerifyOrReturnError(nullptr != tcProvider, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + ReturnErrorOnFailure(tcProvider->GetAcceptance(outTermsAndConditions)); + + return aEncoder.Encode(outTermsAndConditions.ValueOr(TermsAndConditions(0, 0)).GetValue()); + } + case TCAcknowledgementsRequired::Id: { + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + bool acknowledgementsRequired; + + VerifyOrReturnError(nullptr != tcProvider, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + ReturnErrorOnFailure(tcProvider->GetAcknowledgementsRequired(acknowledgementsRequired)); + + return aEncoder.Encode(acknowledgementsRequired); + } + case TCUpdateDeadline::Id: { + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + Optional outUpdateAcceptanceDeadline; + + VerifyOrReturnError(nullptr != tcProvider, CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + ReturnErrorOnFailure(tcProvider->GetUpdateAcceptanceDeadline(outUpdateAcceptanceDeadline)); + + if (!outUpdateAcceptanceDeadline.HasValue()) + { + return aEncoder.EncodeNull(); + } + + return aEncoder.Encode(outUpdateAcceptanceDeadline.Value()); + } +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED default: { break; } @@ -144,6 +206,73 @@ CHIP_ERROR GeneralCommissioningAttrAccess::ReadSupportsConcurrentConnection(Attr return aEncoder.Encode(supportsConcurrentConnection); } +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED +typedef struct sTermsAndConditionsState +{ + Optional acceptance; + bool acknowledgementsRequired; + Optional requirements; + Optional updateAcceptanceDeadline; +} TermsAndConditionsState; + +CHIP_ERROR GetTermsAndConditionsAttributeState(TermsAndConditionsProvider * tcProvider, + TermsAndConditionsState & outTermsAndConditionsState) +{ + TermsAndConditionsState termsAndConditionsState; + + ReturnErrorOnFailure(tcProvider->GetAcceptance(termsAndConditionsState.acceptance)); + ReturnErrorOnFailure(tcProvider->GetAcknowledgementsRequired(termsAndConditionsState.acknowledgementsRequired)); + ReturnErrorOnFailure(tcProvider->GetRequirements(termsAndConditionsState.requirements)); + ReturnErrorOnFailure(tcProvider->GetUpdateAcceptanceDeadline(termsAndConditionsState.updateAcceptanceDeadline)); + + outTermsAndConditionsState = termsAndConditionsState; + return CHIP_NO_ERROR; +} + +void NotifyTermsAndConditionsAttributeChangeIfRequired(const TermsAndConditionsState & initialState, + const TermsAndConditionsState & updatedState) +{ + // Notify on TCAcknowledgementsRequired change + if (initialState.acknowledgementsRequired != updatedState.acknowledgementsRequired) + { + MatterReportingAttributeChangeCallback(kRootEndpointId, GeneralCommissioning::Id, TCAcknowledgementsRequired::Id); + } + + // Notify on TCAcceptedVersion change + if ((initialState.acceptance.HasValue() != updatedState.acceptance.HasValue()) || + (initialState.acceptance.HasValue() && + (initialState.acceptance.Value().GetVersion() != updatedState.acceptance.Value().GetVersion()))) + { + MatterReportingAttributeChangeCallback(kRootEndpointId, GeneralCommissioning::Id, TCAcceptedVersion::Id); + } + + // Notify on TCAcknowledgements change + if ((initialState.acceptance.HasValue() != updatedState.acceptance.HasValue()) || + (initialState.acceptance.HasValue() && + (initialState.acceptance.Value().GetValue() != updatedState.acceptance.Value().GetValue()))) + { + MatterReportingAttributeChangeCallback(kRootEndpointId, GeneralCommissioning::Id, TCAcknowledgements::Id); + } + + // Notify on TCRequirements change + if ((initialState.requirements.HasValue() != updatedState.requirements.HasValue()) || + (initialState.requirements.HasValue() && + (initialState.requirements.Value().GetVersion() != updatedState.requirements.Value().GetVersion() || + initialState.requirements.Value().GetValue() != updatedState.requirements.Value().GetValue()))) + { + MatterReportingAttributeChangeCallback(kRootEndpointId, GeneralCommissioning::Id, TCMinRequiredVersion::Id); + } + + // Notify on TCUpdateDeadline change + if ((initialState.updateAcceptanceDeadline.HasValue() != updatedState.updateAcceptanceDeadline.HasValue()) || + (initialState.updateAcceptanceDeadline.HasValue() && + (initialState.updateAcceptanceDeadline.Value() != updatedState.updateAcceptanceDeadline.Value()))) + { + MatterReportingAttributeChangeCallback(kRootEndpointId, GeneralCommissioning::Id, TCUpdateDeadline::Id); + } +} +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + } // anonymous namespace bool emberAfGeneralCommissioningClusterArmFailSafeCallback(app::CommandHandler * commandObj, @@ -218,60 +347,115 @@ 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 + +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + + // Ensure required terms and conditions have been accepted, then attempt to commit + if (nullptr != tcProvider) { - 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())) + Optional requiredTermsAndConditionsMaybe; + Optional acceptedTermsAndConditionsMaybe; + + CheckSuccess(tcProvider->GetRequirements(requiredTermsAndConditionsMaybe), Failure); + CheckSuccess(tcProvider->GetAcceptance(acceptedTermsAndConditionsMaybe), Failure); + + if (requiredTermsAndConditionsMaybe.HasValue() && !acceptedTermsAndConditionsMaybe.HasValue()) { - response.errorCode = CommissioningErrorEnum::kInvalidAuthentication; - ChipLogError(FailSafe, "GeneralCommissioning: Got commissioning complete in invalid security context"); + response.errorCode = CommissioningErrorEnum::kTCAcknowledgementsNotReceived; + commandObj->AddResponse(commandPath, response); + return true; } - else + + if (requiredTermsAndConditionsMaybe.HasValue() && acceptedTermsAndConditionsMaybe.HasValue()) { - if (failSafe.NocCommandHasBeenInvoked()) + TermsAndConditions requiredTermsAndConditions = requiredTermsAndConditionsMaybe.Value(); + TermsAndConditions acceptedTermsAndConditions = acceptedTermsAndConditionsMaybe.Value(); + + if (!requiredTermsAndConditions.ValidateVersion(acceptedTermsAndConditions)) { - 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); + response.errorCode = CommissioningErrorEnum::kTCMinVersionNotMet; + commandObj->AddResponse(commandPath, response); + return true; } - /* - * 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); + if (!requiredTermsAndConditions.ValidateValue(acceptedTermsAndConditions)) + { + response.errorCode = CommissioningErrorEnum::kRequiredTCNotAccepted; + commandObj->AddResponse(commandPath, response); + return true; + } + } - Breadcrumb::Set(commandPath.mEndpointId, 0); - response.errorCode = CommissioningErrorEnum::kOk; + if (failSafe.UpdateTermsAndConditionsHasBeenInvoked()) + { + // Commit terms and conditions acceptance on commissioning complete + err = tcProvider->CommitAcceptance(); + if (err != CHIP_NO_ERROR) + { + 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_TERMS_AND_CONDITIONS_REQUIRED - commandObj->AddResponse(commandPath, response); + 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; + } + + // 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); + } + + // 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 +512,77 @@ bool emberAfGeneralCommissioningClusterSetRegulatoryConfigCallback(app::CommandH return true; } +bool emberAfGeneralCommissioningClusterSetTCAcknowledgementsCallback( + chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, + const GeneralCommissioning::Commands::SetTCAcknowledgements::DecodableType & commandData) +{ + MATTER_TRACE_SCOPE("SetTCAcknowledgements", "GeneralCommissioning"); + +#if CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + auto & failSafeContext = Server::GetInstance().GetFailSafeContext(); + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + + if (nullptr == tcProvider) + { + commandObj->AddStatus(commandPath, Protocols::InteractionModel::Status::Failure); + return true; + } + + Optional requiredTermsAndConditionsMaybe; + Optional previousAcceptedTermsAndConditionsMaybe; + CheckSuccess(tcProvider->GetRequirements(requiredTermsAndConditionsMaybe), Failure); + CheckSuccess(tcProvider->GetAcceptance(previousAcceptedTermsAndConditionsMaybe), Failure); + TermsAndConditions acceptedTermsAndConditions = TermsAndConditions(commandData.TCUserResponse, commandData.TCVersion); + Optional acceptedTermsAndConditionsPresent = Optional(acceptedTermsAndConditions); + + Commands::SetTCAcknowledgementsResponse::Type response; + + if (requiredTermsAndConditionsMaybe.HasValue()) + { + TermsAndConditions requiredTermsAndConditions = requiredTermsAndConditionsMaybe.Value(); + + if (!requiredTermsAndConditions.ValidateVersion(acceptedTermsAndConditions)) + { + response.errorCode = CommissioningErrorEnum::kTCMinVersionNotMet; + commandObj->AddResponse(commandPath, response); + return true; + } + + if (!requiredTermsAndConditions.ValidateValue(acceptedTermsAndConditions)) + { + response.errorCode = CommissioningErrorEnum::kRequiredTCNotAccepted; + commandObj->AddResponse(commandPath, response); + return true; + } + } + + if (previousAcceptedTermsAndConditionsMaybe != acceptedTermsAndConditionsPresent) + { + TermsAndConditionsState initialState, updatedState; + CheckSuccess(GetTermsAndConditionsAttributeState(tcProvider, initialState), Failure); + CheckSuccess(tcProvider->SetAcceptance(acceptedTermsAndConditionsPresent), Failure); + CheckSuccess(GetTermsAndConditionsAttributeState(tcProvider, updatedState), Failure); + NotifyTermsAndConditionsAttributeChangeIfRequired(initialState, updatedState); + + // Commit or defer based on fail-safe state + if (!failSafeContext.IsFailSafeArmed()) + { + CheckSuccess(tcProvider->CommitAcceptance(), Failure); + } + else + { + failSafeContext.SetUpdateTermsAndConditionsHasBeenInvoked(); + } + } + + response.errorCode = CommissioningErrorEnum::kOk; + commandObj->AddResponse(commandPath, response); + return true; + +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_REQUIRED + return true; +} + namespace { void OnPlatformEventHandler(const DeviceLayer::ChipDeviceEvent * event, intptr_t arg) { @@ -335,16 +590,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_TERMS_AND_CONDITIONS_REQUIRED + // Clear terms and conditions acceptance on failsafe timer expiration + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + TermsAndConditionsState initialState, updatedState; + VerifyOrReturn(nullptr != tcProvider); + VerifyOrReturn(CHIP_NO_ERROR == GetTermsAndConditionsAttributeState(tcProvider, initialState)); + VerifyOrReturn(CHIP_NO_ERROR == tcProvider->RevertAcceptance()); + VerifyOrReturn(CHIP_NO_ERROR == GetTermsAndConditionsAttributeState(tcProvider, updatedState)); + NotifyTermsAndConditionsAttributeChangeIfRequired(initialState, updatedState); +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_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_TERMS_AND_CONDITIONS_REQUIRED + TermsAndConditionsProvider * tcProvider = TermsAndConditionsManager::GetInstance(); + TermsAndConditionsState initialState, updatedState; + VerifyOrReturn(nullptr != tcProvider); + VerifyOrReturn(CHIP_NO_ERROR == GetTermsAndConditionsAttributeState(tcProvider, initialState)); + VerifyOrReturn(CHIP_NO_ERROR == tcProvider->ResetAcceptance()); + VerifyOrReturn(CHIP_NO_ERROR == GetTermsAndConditionsAttributeState(tcProvider, updatedState)); + NotifyTermsAndConditionsAttributeChangeIfRequired(initialState, updatedState); +#endif // CHIP_CONFIG_TERMS_AND_CONDITIONS_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/common_flags.gni b/src/app/common_flags.gni index 30678dfe330f2f..7fd111c371b548 100644 --- a/src/app/common_flags.gni +++ b/src/app/common_flags.gni @@ -25,6 +25,10 @@ declare_args() { # communicated to OperationalSessionSetup API consumers. chip_enable_busy_handling_for_operational_session_setup = true + # Controls whether the device commissioning process requires the user to + # acknowledge terms and conditions during commissioning. + chip_terms_and_conditions_required = false + # This controls if more logging is supposed to be enabled into the data models. # This is an optimization for small-flash size devices as extra logging requires # additional flash for messages & code for formatting. diff --git a/src/app/server/BUILD.gn b/src/app/server/BUILD.gn index fb25ae0f38bf6b..040c7b2227ff2a 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. @@ -25,6 +25,22 @@ config("server_config") { } } +source_set("terms_and_conditions") { + sources = [ + "DefaultTermsAndConditionsProvider.cpp", + "DefaultTermsAndConditionsProvider.h", + "TermsAndConditionsManager.cpp", + "TermsAndConditionsManager.h", + "TermsAndConditionsProvider.h", + ] + + public_deps = [ + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/support", + "${chip_root}/src/protocols", + ] +} + static_library("server") { output_name = "libCHIPAppServer" @@ -69,6 +85,10 @@ static_library("server") { "${chip_root}/src/transport", ] + if (chip_terms_and_conditions_required) { + public_deps += [ ":terms_and_conditions" ] + } + # TODO: Server.cpp uses TestGroupData.h. Unsure why test code would be in such a central place # This dependency is split since it should probably be removed (or naming should # be updated if this is not really "testing" even though headers are Test*.h) diff --git a/src/app/server/DefaultTermsAndConditionsProvider.cpp b/src/app/server/DefaultTermsAndConditionsProvider.cpp new file mode 100644 index 00000000000000..36431aea456128 --- /dev/null +++ b/src/app/server/DefaultTermsAndConditionsProvider.cpp @@ -0,0 +1,251 @@ +/* + * 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 +#include +#include + +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 kSerializationSchemaMinimumVersion = 1; +constexpr uint8_t kSerializationSchemaCurrentVersion = 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 * inPersistentStorageDelegate) +{ + VerifyOrReturnValue(nullptr != inPersistentStorageDelegate, CHIP_ERROR_INVALID_ARGUMENT); + + mStorageDelegate = inPersistentStorageDelegate; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Delete() +{ + VerifyOrReturnValue(nullptr != mStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + const chip::StorageKeyName kStorageKey = chip::DefaultStorageKeyAllocator::TermsAndConditionsAcceptance(); + ReturnErrorOnFailure(mStorageDelegate->SyncDeleteKeyValue(kStorageKey.KeyName())); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsStorageDelegate::Get(Optional & outTermsAndConditions) +{ + VerifyOrReturnValue(nullptr != mStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + 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); + VerifyOrReturnValue(CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err || CHIP_NO_ERROR == err, err); + + if (CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err) + { + outTermsAndConditions.ClearValue(); + return CHIP_NO_ERROR; + } + + tlvReader.Init(buffer, bufferSize); + ReturnErrorOnFailure(tlvReader.Next(chip::TLV::kTLVType_Structure, chip::TLV::AnonymousTag())); + ReturnErrorOnFailure(tlvReader.EnterContainer(tlvContainer)); + ReturnErrorOnFailure(tlvReader.Next(kSerializationVersionTag)); + ReturnErrorOnFailure(tlvReader.Get(serializationVersion)); + + if (serializationVersion < kSerializationSchemaMinimumVersion) + { + ChipLogError(AppServer, "The terms and conditions datastore schema (%hhu) is newer than oldest compatible schema (%hhu)", + serializationVersion, kSerializationSchemaMinimumVersion); + return CHIP_IM_GLOBAL_STATUS(ConstraintError); + } + + if (serializationVersion != kSerializationSchemaCurrentVersion) + { + ChipLogDetail(AppServer, "The terms and conditions datastore schema (%hhu) differs from current schema (%hhu)", + serializationVersion, kSerializationSchemaCurrentVersion); + } + + ReturnErrorOnFailure(tlvReader.Next(kAcceptedAcknowledgementsTag)); + ReturnErrorOnFailure(tlvReader.Get(acknowledgements)); + ReturnErrorOnFailure(tlvReader.Next(kAcceptedAcknowledgementsVersionTag)); + ReturnErrorOnFailure(tlvReader.Get(acknowledgementsVersion)); + ReturnErrorOnFailure(tlvReader.ExitContainer(tlvContainer)); + + outTermsAndConditions = Optional(TermsAndConditions(acknowledgements, 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; + + VerifyOrReturnValue(nullptr != mStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + tlvWriter.Init(buffer); + ReturnErrorOnFailure(tlvWriter.StartContainer(chip::TLV::AnonymousTag(), chip::TLV::kTLVType_Structure, tlvContainer)); + ReturnErrorOnFailure(tlvWriter.Put(kSerializationVersionTag, kSerializationSchemaCurrentVersion)); + ReturnErrorOnFailure(tlvWriter.Put(kAcceptedAcknowledgementsTag, inTermsAndConditions.GetValue())); + ReturnErrorOnFailure(tlvWriter.Put(kAcceptedAcknowledgementsVersionTag, inTermsAndConditions.GetVersion())); + ReturnErrorOnFailure(tlvWriter.EndContainer(tlvContainer)); + ReturnErrorOnFailure(tlvWriter.Finalize()); + + const chip::StorageKeyName kStorageKey = chip::DefaultStorageKeyAllocator::TermsAndConditionsAcceptance(); + ReturnErrorOnFailure( + mStorageDelegate->SyncSetKeyValue(kStorageKey.KeyName(), buffer, static_cast(tlvWriter.GetLengthWritten()))); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::Init( + TermsAndConditionsStorageDelegate * inStorageDelegate, + const chip::Optional & inRequiredTermsAndConditions) +{ + VerifyOrReturnValue(nullptr != inStorageDelegate, CHIP_ERROR_INVALID_ARGUMENT); + + mTermsAndConditionsStorageDelegate = inStorageDelegate; + mRequiredAcknowledgements = inRequiredTermsAndConditions; + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::CommitAcceptance() +{ + VerifyOrReturnValue(nullptr != mTermsAndConditionsStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + // No terms and conditions to commit + VerifyOrReturnValue(mTemporalAcceptance.HasValue(), CHIP_NO_ERROR); + + ReturnErrorOnFailure(mTermsAndConditionsStorageDelegate->Set(mTemporalAcceptance.Value())); + ChipLogProgress(AppServer, "Terms and conditions have been committed"); + mTemporalAcceptance.ClearValue(); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::GetAcceptance(Optional & outTermsAndConditions) const +{ + VerifyOrReturnValue(nullptr != mTermsAndConditionsStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + // 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); + VerifyOrReturnValue(CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err || CHIP_NO_ERROR == err, err); + + if (CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND == err) + { + ChipLogError(AppServer, "No terms and conditions have been accepted"); + outTermsAndConditions.ClearValue(); + return CHIP_NO_ERROR; + } + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::GetAcknowledgementsRequired(bool & outAcknowledgementsRequired) const +{ + Optional requiredTermsAndConditionsMaybe; + ReturnErrorOnFailure(GetRequirements(requiredTermsAndConditionsMaybe)); + + if (!requiredTermsAndConditionsMaybe.HasValue()) + { + outAcknowledgementsRequired = false; + return CHIP_NO_ERROR; + } + + Optional acceptedTermsAndConditionsMaybe; + ReturnErrorOnFailure(GetAcceptance(acceptedTermsAndConditionsMaybe)); + + if (!acceptedTermsAndConditionsMaybe.HasValue()) + { + outAcknowledgementsRequired = true; + return CHIP_NO_ERROR; + } + + TermsAndConditions requiredTermsAndConditions = requiredTermsAndConditionsMaybe.Value(); + TermsAndConditions acceptedTermsAndConditions = acceptedTermsAndConditionsMaybe.Value(); + outAcknowledgementsRequired = requiredTermsAndConditions.Validate(acceptedTermsAndConditions); + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::GetRequirements(Optional & outTermsAndConditions) const +{ + outTermsAndConditions = mRequiredAcknowledgements; + return CHIP_NO_ERROR; +} + +CHIP_ERROR +chip::app::DefaultTermsAndConditionsProvider::GetUpdateAcceptanceDeadline(Optional & outUpdateAcceptanceDeadline) const +{ + // No-op stub implementation. This feature is not implemented in this default implementation. + outUpdateAcceptanceDeadline = Optional(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::ResetAcceptance() +{ + VerifyOrReturnValue(nullptr != mTermsAndConditionsStorageDelegate, CHIP_ERROR_UNINITIALIZED); + + (void) mTermsAndConditionsStorageDelegate->Delete(); + ReturnErrorOnFailure(RevertAcceptance()); + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::RevertAcceptance() +{ + mTemporalAcceptance.ClearValue(); + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::DefaultTermsAndConditionsProvider::SetAcceptance(const Optional & inTermsAndConditions) +{ + 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..8bc3d0761b3e21 --- /dev/null +++ b/src/app/server/DefaultTermsAndConditionsProvider.h @@ -0,0 +1,152 @@ +/* + * + * 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_UNINITIALIZED if the storage delegate is not properly initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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_UNINITIALIZED if the storage delegate is not properly initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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_UNINITIALIZED if the storage delegate is not properly initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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 * 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. + * @param[in] inRequiredTermsAndConditions The required terms and conditions that must be met. + */ + CHIP_ERROR Init(TermsAndConditionsStorageDelegate * inStorageDelegate, + const Optional & inRequiredTermsAndConditions); + + CHIP_ERROR CommitAcceptance() override; + + CHIP_ERROR GetAcceptance(Optional & outTermsAndConditions) const override; + + CHIP_ERROR GetAcknowledgementsRequired(bool & outAcknowledgementsRequired) const override; + + CHIP_ERROR GetRequirements(Optional & outTermsAndConditions) const override; + + CHIP_ERROR GetUpdateAcceptanceDeadline(Optional & outUpdateAcceptanceDeadline) 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/TermsAndConditionsManager.cpp b/src/app/server/TermsAndConditionsManager.cpp new file mode 100644 index 00000000000000..4946bf37d2611f --- /dev/null +++ b/src/app/server/TermsAndConditionsManager.cpp @@ -0,0 +1,80 @@ +/* + * + * 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 "TermsAndConditionsManager.h" + +#include "DefaultTermsAndConditionsProvider.h" + +static chip::app::TermsAndConditionsManager sTermsAndConditionsManager; +static chip::app::DefaultTermsAndConditionsProvider sTermsAndConditionsProviderInstance; +static chip::app::DefaultTermsAndConditionsStorageDelegate sTermsAndConditionsStorageDelegateInstance; + +chip::app::TermsAndConditionsManager * chip::app::TermsAndConditionsManager::GetInstance() +{ + return &sTermsAndConditionsManager; +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::Init(chip::PersistentStorageDelegate * inPersistentStorageDelegate, + const Optional & inRequiredTermsAndConditions) +{ + ReturnErrorOnFailure(sTermsAndConditionsStorageDelegateInstance.Init(inPersistentStorageDelegate)); + ReturnErrorOnFailure( + sTermsAndConditionsProviderInstance.Init(&sTermsAndConditionsStorageDelegateInstance, inRequiredTermsAndConditions)); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::CommitAcceptance() +{ + return sTermsAndConditionsProviderInstance.CommitAcceptance(); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::GetAcceptance(Optional & outTermsAndConditions) const +{ + return sTermsAndConditionsProviderInstance.GetAcceptance(outTermsAndConditions); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::GetAcknowledgementsRequired(bool & outAcknowledgementsRequired) const +{ + return sTermsAndConditionsProviderInstance.GetAcknowledgementsRequired(outAcknowledgementsRequired); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::GetRequirements(Optional & outTermsAndConditions) const +{ + return sTermsAndConditionsProviderInstance.GetRequirements(outTermsAndConditions); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::GetUpdateAcceptanceDeadline(Optional & outUpdateAcceptanceDeadline) const +{ + return sTermsAndConditionsProviderInstance.GetUpdateAcceptanceDeadline(outUpdateAcceptanceDeadline); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::ResetAcceptance() +{ + return sTermsAndConditionsProviderInstance.ResetAcceptance(); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::RevertAcceptance() +{ + return sTermsAndConditionsProviderInstance.RevertAcceptance(); +} + +CHIP_ERROR chip::app::TermsAndConditionsManager::SetAcceptance(const Optional & inTermsAndConditions) +{ + return sTermsAndConditionsProviderInstance.SetAcceptance(inTermsAndConditions); +} diff --git a/src/app/server/TermsAndConditionsManager.h b/src/app/server/TermsAndConditionsManager.h new file mode 100644 index 00000000000000..02101bbec46425 --- /dev/null +++ b/src/app/server/TermsAndConditionsManager.h @@ -0,0 +1,45 @@ +/* + * + * 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 "TermsAndConditionsProvider.h" + +namespace chip { +namespace app { + +class TermsAndConditionsManager : public TermsAndConditionsProvider +{ +public: + static TermsAndConditionsManager * GetInstance(); + CHIP_ERROR Init(PersistentStorageDelegate * inPersistentStorageDelegate, + const Optional & inRequiredTermsAndConditions); + CHIP_ERROR CommitAcceptance(); + CHIP_ERROR GetAcceptance(Optional & outTermsAndConditions) const; + CHIP_ERROR GetAcknowledgementsRequired(bool & outAcknowledgementsRequired) const; + CHIP_ERROR GetRequirements(Optional & outTermsAndConditions) const; + CHIP_ERROR GetUpdateAcceptanceDeadline(Optional & outUpdateAcceptanceDeadline) const; + CHIP_ERROR ResetAcceptance(); + CHIP_ERROR RevertAcceptance(); + CHIP_ERROR SetAcceptance(const Optional & inTermsAndConditions); +}; + +} // namespace app +} // namespace chip diff --git a/src/app/server/TermsAndConditionsProvider.h b/src/app/server/TermsAndConditionsProvider.h new file mode 100644 index 00000000000000..cb0b6f0b8f088a --- /dev/null +++ b/src/app/server/TermsAndConditionsProvider.h @@ -0,0 +1,224 @@ +/* + * + * 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 { + +/** + * @brief Represents a pair of terms and conditions value and version. + * + * This class encapsulates terms and conditions with methods to validate a user's accepted value and version against required + * criteria. + */ +class TermsAndConditions +{ +public: + TermsAndConditions(uint16_t inValue, uint16_t inVersion) : value(inValue), version(inVersion) {} + + bool operator==(const TermsAndConditions & other) const { return value == other.value && version == other.version; } + bool operator!=(const TermsAndConditions & other) const { return !(*this == other); } + + /** + * @brief Retrieves the terms and conditions value (accepted bits). + * + * @return The value of the terms and conditions. + */ + uint16_t GetValue() const { return value; } + + /** + * @brief Retrieves the terms and conditions version. + * + * @return The version of the terms and conditions. + */ + uint16_t GetVersion() const { return version; } + + /** + * @brief Validates the terms and conditions value. + * + * Checks whether all required bits are set in the accepted terms and conditions. + * + * @param acceptedTermsAndConditions The user's accepted terms and conditions. + * @return True if all required bits are set, false otherwise. + */ + bool ValidateValue(const TermsAndConditions & acceptedTermsAndConditions) const + { + // Check if all required bits are set in the user-accepted value. + return ((value & acceptedTermsAndConditions.GetValue()) == value); + } + + /** + * @brief Validates the terms and conditions version. + * + * Checks whether the accepted version is greater than or equal to the required version. + * + * @param acceptedTermsAndConditions The user's accepted terms and conditions. + * @return True if the accepted version is valid, false otherwise. + */ + bool ValidateVersion(const TermsAndConditions & acceptedTermsAndConditions) const + { + // Check if the version is below the minimum required version. + return (acceptedTermsAndConditions.GetVersion() >= version); + } + + /** + * @brief Validates the terms and conditions. + * + * Combines validation of both value and version to ensure compliance with requirements. + * + * @param acceptedTermsAndConditions The user's accepted terms and conditions. + * @return True if both value and version validations pass, false otherwise. + */ + bool Validate(const TermsAndConditions & acceptedTermsAndConditions) const + { + return ValidateVersion(acceptedTermsAndConditions) && ValidateValue(acceptedTermsAndConditions); + } + +private: + const uint16_t value; + const uint16_t version; +}; + +/** + * @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 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_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + virtual CHIP_ERROR GetAcceptance(Optional & outTermsAndConditions) const = 0; + + /** + * @brief Determines if acknowledgments are required. + * + * @param[out] outAcknowledgementsRequired True if acknowledgments are required, false otherwise. + * + * @retval CHIP_NO_ERROR if successful. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + virtual CHIP_ERROR GetAcknowledgementsRequired(bool & outAcknowledgementsRequired) 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_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + virtual CHIP_ERROR GetRequirements(Optional & outTermsAndConditions) const = 0; + + /** + * @brief Retrieves the deadline for accepting updated terms and conditions. + * + * This method retrieves the deadline by which the user must accept updated terms and conditions. + * If no deadline is set, it returns an empty `Optional`. + * + * @param[out] outUpdateAcceptanceDeadline The deadline (in seconds) by which updated terms must be accepted. + * Returns empty Optional if no deadline is set. + * + * @retval CHIP_NO_ERROR if the deadline was successfully retrieved or no deadline was found. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + virtual CHIP_ERROR GetUpdateAcceptanceDeadline(Optional & outUpdateAcceptanceDeadline) 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_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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_INVALID_ARGUMENT if the provided terms and conditions are invalid. + * @retval CHIP_ERROR_UNINITIALIZED if the module has not been initialized. + * @retval CHIP_ERROR_* for other errors. + */ + 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 912c1e32a0ef5a..1923573765101a 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. @@ -211,6 +211,7 @@ chip_test_suite("tests") { "TestDataModelSerialization.cpp", "TestDefaultOTARequestorStorage.cpp", "TestDefaultSafeAttributePersistenceProvider.cpp", + "TestDefaultTermsAndConditionsProvider.cpp", "TestDefaultThreadNetworkDirectoryStorage.cpp", "TestEcosystemInformationCluster.cpp", "TestEventLoggingNoUTCTime.cpp", @@ -252,6 +253,8 @@ 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/server:terms_and_conditions", "${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..dc2a66278aa5d2 --- /dev/null +++ b/src/app/tests/TestDefaultTermsAndConditionsProvider.cpp @@ -0,0 +1,435 @@ +/* + * + * 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(chip::app::TermsAndConditions(1, 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(chip::app::TermsAndConditions(0b1111'1111'1111'1111, 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(chip::app::TermsAndConditions(1, 1)); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional newTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 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().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); + + err = defaultTermsAndConditionsProvider.CommitAcceptance(); + err = defaultTermsAndConditionsProvider.GetAcceptance(outTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + EXPECT_EQ(1, outTermsAndConditions.Value().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); + + 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().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); +} + +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(chip::app::TermsAndConditions(1, 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().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); +} + +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(chip::app::TermsAndConditions(1, 1)); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 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().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); +} + +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(chip::app::TermsAndConditions(1, 1)); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 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().GetValue()); + EXPECT_EQ(1, outTermsAndConditions.Value().GetVersion()); + + 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(chip::app::TermsAndConditions(1, 1)); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + chip::Optional acceptedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 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().GetValue()); + EXPECT_EQ(1, outAcknowledgementTermsAndConditions.Value().GetVersion()); + + 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().GetValue()); + EXPECT_EQ(1, outRequiredTermsAndConditions.Value().GetVersion()); +} + +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(chip::app::TermsAndConditions(1, 1)); + err = defaultTermsAndConditionsProvider.Init(&defaultTermsAndConditionsStorageDelegate, requiredTermsAndConditions); + EXPECT_EQ(CHIP_NO_ERROR, err); + + // Set acceptance + chip::Optional acceptedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(0b1, 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().GetValue(), acceptedTermsAndConditions.Value().GetValue()); + EXPECT_EQ(outTermsAndConditions.Value().GetVersion(), acceptedTermsAndConditions.Value().GetVersion()); + + // Set updated value + chip::Optional updatedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(0b11, 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().GetValue(), updatedTermsAndConditions.Value().GetValue()); + EXPECT_EQ(outTermsAndConditions.Value().GetVersion(), updatedTermsAndConditions.Value().GetVersion()); + + // 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().GetValue(), acceptedTermsAndConditions.Value().GetValue()); + EXPECT_EQ(outTermsAndConditions.Value().GetVersion(), acceptedTermsAndConditions.Value().GetVersion()); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceWhileMissing) +{ + CHIP_ERROR err; + + chip::TestPersistentStorageDelegate testPersistentStorageDelegate; + chip::app::DefaultTermsAndConditionsStorageDelegate defaultTermsAndConditionsStorageDelegate; + chip::app::DefaultTermsAndConditionsProvider defaultTermsAndConditionsProvider; + + chip::Optional requiredTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 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(chip::app::TermsAndConditions(1, 1)); + chip::Optional requiredTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(0b11, 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().GetValue(), 1); + EXPECT_EQ(outTermsAndConditions.Value().GetVersion(), 1); +} + +TEST(DefaultTermsAndConditionsProvider, TestRevertAcceptanceWhenPreviouslyAcceptedThenUpdatedUnderFailsafe) +{ + CHIP_ERROR err; + + // Initialize unit under test dependency + chip::Optional initiallyAcceptedTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(1, 1)); + TestTermsAndConditionsStorageDelegate testTermsAndConditionsStorageDelegate(initiallyAcceptedTermsAndConditions); + + // Initialize unit under test [Conditions previously accepted] + chip::Optional requiredTermsAndConditions = + chip::Optional(chip::app::TermsAndConditions(0b11, 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(chip::app::TermsAndConditions(0b111, 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().GetValue(), initiallyAcceptedTermsAndConditions.Value().GetValue()); + EXPECT_EQ(outTermsAndConditions.Value().GetVersion(), initiallyAcceptedTermsAndConditions.Value().GetVersion()); +} 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/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