From b568e34d3aa72cf201e53118a6ca5f0fb83a178a Mon Sep 17 00:00:00 2001 From: Marc Lepage Date: Fri, 15 Oct 2021 15:02:42 -0400 Subject: [PATCH] Add initial prototype hookup of AccessControl - Hook up to interaction model and messaging layer - Not complete, always allows actions - Completes issue #10236 --- src/app/BUILD.gn | 1 + src/app/InteractionModelEngine.h | 15 ++-- src/app/ReadHandler.h | 5 ++ src/app/WriteHandler.cpp | 4 +- src/app/reporting/Engine.cpp | 14 ++-- src/app/reporting/Engine.h | 2 +- src/app/tests/TestReadInteraction.cpp | 2 +- src/app/tests/TestWriteInteraction.cpp | 2 +- .../tests/integration/chip_im_initiator.cpp | 4 +- .../tests/integration/chip_im_responder.cpp | 4 +- .../util/ember-compatibility-functions.cpp | 78 +++++++++++++++++-- .../tests/data_model/TestCommands.cpp | 4 +- src/messaging/BUILD.gn | 1 + src/messaging/ExchangeContext.h | 20 +++++ 14 files changed, 128 insertions(+), 28 deletions(-) diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index 24ec4557a92876..80e362b9d781bd 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -112,6 +112,7 @@ static_library("app") { public_deps = [ ":app_buildconfig", + "${chip_root}/src/access", "${chip_root}/src/lib/support", "${chip_root}/src/messaging", "${chip_root}/src/protocols/secure_channel", diff --git a/src/app/InteractionModelEngine.h b/src/app/InteractionModelEngine.h index da55eaf3a02fd4..a4e1608af98b16 100644 --- a/src/app/InteractionModelEngine.h +++ b/src/app/InteractionModelEngine.h @@ -240,19 +240,20 @@ bool ServerClusterCommandExists(const ConcreteCommandPath & aCommandPath); * This function is implemented by CHIP as a part of cluster data storage & management. * The apWriter and apDataExists can be nullptr. * - * @param[in] aPath The concrete path of the data being read. - * @param[in] apWriter The TLVWriter for holding cluster data. Can be a nullptr if the caller does not care - * the exact value of the attribute. - * @param[out] apDataExists Tell whether the cluster data exist on server. Can be a nullptr if the caller does not care - * whether the data exists. + * @param[in] aSubjectDescriptor The subject descriptor. + * @param[in] aPath The concrete path of the data being read. + * @param[in] apWriter The TLVWriter for holding cluster data. Can be a nullptr if the caller does not care + * the exact value of the attribute. + * @param[out] apDataExists Tell whether the cluster data exist on server. Can be a nullptr if the caller does not care + * whether the data exists. * * @retval CHIP_NO_ERROR on success */ -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists); +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists); /** * TODO: Document. */ -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler); +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler); } // namespace app } // namespace chip diff --git a/src/app/ReadHandler.h b/src/app/ReadHandler.h index 1d60dc82599c58..77d4c0db3bfefd 100644 --- a/src/app/ReadHandler.h +++ b/src/app/ReadHandler.h @@ -136,6 +136,11 @@ class ReadHandler : public Messaging::ExchangeDelegate NodeId GetInitiatorNodeId() const { return mInitiatorNodeId; } FabricIndex GetFabricIndex() const { return mFabricIndex; } + const access::SubjectDescriptor GetSubjectDescriptor() const + { + return mpExchangeCtx ? mpExchangeCtx->GetSubjectDescriptor() : access::SubjectDescriptor(); + } + private: friend class TestReadInteraction; enum class HandlerState diff --git a/src/app/WriteHandler.cpp b/src/app/WriteHandler.cpp index 7145f8c275536d..234215437394e4 100644 --- a/src/app/WriteHandler.cpp +++ b/src/app/WriteHandler.cpp @@ -109,6 +109,8 @@ CHIP_ERROR WriteHandler::SendWriteResponse() CHIP_ERROR WriteHandler::ProcessAttributeDataList(TLV::TLVReader & aAttributeDataListReader) { CHIP_ERROR err = CHIP_NO_ERROR; + const access::SubjectDescriptor subjectDescriptor = mpExchangeCtx->GetSubjectDescriptor(); + while (CHIP_NO_ERROR == (err = aAttributeDataListReader.Next())) { chip::TLV::TLVReader dataReader; @@ -151,7 +153,7 @@ CHIP_ERROR WriteHandler::ProcessAttributeDataList(TLV::TLVReader & aAttributeDat err = element.GetData(&dataReader); SuccessOrExit(err); - err = WriteSingleClusterData(clusterInfo, dataReader, this); + err = WriteSingleClusterData(subjectDescriptor, clusterInfo, dataReader, this); SuccessOrExit(err); } diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp index f5562d151c72a1..bf7971a8827d73 100644 --- a/src/app/reporting/Engine.cpp +++ b/src/app/reporting/Engine.cpp @@ -62,7 +62,7 @@ EventNumber Engine::CountEvents(ReadHandler * apReadHandler, EventNumber * apIni } CHIP_ERROR -Engine::RetrieveClusterData(AttributeDataList::Builder & aAttributeDataList, ClusterInfo & aClusterInfo) +Engine::RetrieveClusterData(const access::SubjectDescriptor & aSubjectDescriptor, AttributeDataList::Builder & aAttributeDataList, ClusterInfo & aClusterInfo) { CHIP_ERROR err = CHIP_NO_ERROR; ConcreteAttributePath path(aClusterInfo.mEndpointId, aClusterInfo.mClusterId, aClusterInfo.mFieldId); @@ -79,7 +79,7 @@ Engine::RetrieveClusterData(AttributeDataList::Builder & aAttributeDataList, Clu ChipLogDetail(DataManagement, " Cluster %" PRIx32 ", Field %" PRIx32 " is dirty", aClusterInfo.mClusterId, aClusterInfo.mFieldId); - err = ReadSingleClusterData(path, attributeDataElementBuilder.GetWriter(), nullptr /* data exists */); + err = ReadSingleClusterData(aSubjectDescriptor, path, attributeDataElementBuilder.GetWriter(), nullptr /* data exists */); SuccessOrExit(err); attributeDataElementBuilder.MoreClusterData(false); attributeDataElementBuilder.EndOfAttributeDataElement(); @@ -97,19 +97,21 @@ Engine::RetrieveClusterData(AttributeDataList::Builder & aAttributeDataList, Clu CHIP_ERROR Engine::BuildSingleReportDataAttributeDataList(ReportData::Builder & aReportDataBuilder, ReadHandler * apReadHandler) { - CHIP_ERROR err = CHIP_NO_ERROR; + CHIP_ERROR err = CHIP_NO_ERROR; + const access::SubjectDescriptor subjectDescriptor = apReadHandler->GetSubjectDescriptor(); bool attributeClean = true; TLV::TLVWriter backup; aReportDataBuilder.Checkpoint(backup); AttributeDataList::Builder attributeDataList = aReportDataBuilder.CreateAttributeDataListBuilder(); SuccessOrExit(err = aReportDataBuilder.GetError()); + // TODO: Need to handle multiple chunk of message for (auto clusterInfo = apReadHandler->GetAttributeClusterInfolist(); clusterInfo != nullptr; clusterInfo = clusterInfo->mpNext) { if (apReadHandler->IsInitialReport()) { // Retrieve data for this cluster instance and clear its dirty flag. - err = RetrieveClusterData(attributeDataList, *clusterInfo); + err = RetrieveClusterData(subjectDescriptor, attributeDataList, *clusterInfo); VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(DataManagement, " Error retrieving data from cluster, aborting")); attributeClean = false; @@ -120,11 +122,11 @@ CHIP_ERROR Engine::BuildSingleReportDataAttributeDataList(ReportData::Builder & { if (clusterInfo->IsAttributePathSupersetOf(*path)) { - err = RetrieveClusterData(attributeDataList, *path); + err = RetrieveClusterData(subjectDescriptor, attributeDataList, *path); } else if (path->IsAttributePathSupersetOf(*clusterInfo)) { - err = RetrieveClusterData(attributeDataList, *clusterInfo); + err = RetrieveClusterData(subjectDescriptor, attributeDataList, *clusterInfo); } else { diff --git a/src/app/reporting/Engine.h b/src/app/reporting/Engine.h index bcd6547c81a330..c2bb859343b489 100644 --- a/src/app/reporting/Engine.h +++ b/src/app/reporting/Engine.h @@ -94,7 +94,7 @@ class Engine CHIP_ERROR BuildSingleReportDataAttributeDataList(ReportData::Builder & reportDataBuilder, ReadHandler * apReadHandler); CHIP_ERROR BuildSingleReportDataEventList(ReportData::Builder & reportDataBuilder, ReadHandler * apReadHandler); - CHIP_ERROR RetrieveClusterData(AttributeDataList::Builder & aAttributeDataList, ClusterInfo & aClusterInfo); + CHIP_ERROR RetrieveClusterData(const access::SubjectDescriptor & aSubjectDescriptor, AttributeDataList::Builder & aAttributeDataList, ClusterInfo & aClusterInfo); EventNumber CountEvents(ReadHandler * apReadHandler, EventNumber * apInitialEvents); /** diff --git a/src/app/tests/TestReadInteraction.cpp b/src/app/tests/TestReadInteraction.cpp index 0a8507a46ad2a4..e5b7a38ba558dc 100644 --- a/src/app/tests/TestReadInteraction.cpp +++ b/src/app/tests/TestReadInteraction.cpp @@ -231,7 +231,7 @@ class MockInteractionModelApp : public chip::app::InteractionModelDelegate namespace chip { namespace app { -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) { uint64_t version = 0; ChipLogDetail(DataManagement, "TEST Cluster %" PRIx32 ", Field %" PRIx32 " is dirty", aPath.mClusterId, aPath.mAttributeId); diff --git a/src/app/tests/TestWriteInteraction.cpp b/src/app/tests/TestWriteInteraction.cpp index f7444ca0f4b7c0..af97594137abee 100644 --- a/src/app/tests/TestWriteInteraction.cpp +++ b/src/app/tests/TestWriteInteraction.cpp @@ -261,7 +261,7 @@ void TestWriteInteraction::TestWriteHandler(nlTestSuite * apSuite, void * apCont NL_TEST_ASSERT(apSuite, rm->TestGetCountRetransTable() == 0); } -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * aWriteHandler) +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * aWriteHandler) { TLV::TLVWriter writer; writer.Init(attributeDataTLV); diff --git a/src/app/tests/integration/chip_im_initiator.cpp b/src/app/tests/integration/chip_im_initiator.cpp index b5fb2cf0bb531d..9b15b81869fa09 100644 --- a/src/app/tests/integration/chip_im_initiator.cpp +++ b/src/app/tests/integration/chip_im_initiator.cpp @@ -636,7 +636,7 @@ void DispatchSingleClusterResponseCommand(const ConcreteCommandPath & aCommandPa gLastCommandResult = TestCommandResult::kSuccess; } -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) { // We do not really care about the value, just return a not found status code. VerifyOrReturnError(apWriter != nullptr, CHIP_NO_ERROR); @@ -644,7 +644,7 @@ CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWr Protocols::InteractionModel::Status::UnsupportedAttribute); } -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler *) +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler *) { if (aClusterInfo.mClusterId != kTestClusterId || aClusterInfo.mEndpointId != kTestEndpointId) { diff --git a/src/app/tests/integration/chip_im_responder.cpp b/src/app/tests/integration/chip_im_responder.cpp index 0b171750ac06b4..56bba7b03e85c3 100644 --- a/src/app/tests/integration/chip_im_responder.cpp +++ b/src/app/tests/integration/chip_im_responder.cpp @@ -110,7 +110,7 @@ void DispatchSingleClusterResponseCommand(const ConcreteCommandPath & aCommandPa // Nothing todo. } -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) { CHIP_ERROR err = CHIP_NO_ERROR; uint64_t version = 0; @@ -125,7 +125,7 @@ CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWr return err; } -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler) +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler) { CHIP_ERROR err = CHIP_NO_ERROR; AttributePathParams attributePathParams; diff --git a/src/app/util/ember-compatibility-functions.cpp b/src/app/util/ember-compatibility-functions.cpp index 307942e71e86db..0b927b9ae0281c 100644 --- a/src/app/util/ember-compatibility-functions.cpp +++ b/src/app/util/ember-compatibility-functions.cpp @@ -21,6 +21,8 @@ * when calling ember callbacks. */ +#include +#include #include #include #include @@ -48,6 +50,9 @@ using namespace chip; using namespace chip::app; using namespace chip::app::Compatibility; +using chip::access::AccessControl; +using chip::access::Privilege; + namespace chip { namespace app { namespace Compatibility { @@ -186,7 +191,7 @@ bool ServerClusterCommandExists(const ConcreteCommandPath & aCommandPath) return emberAfContainsServer(aCommandPath.mEndpointId, aCommandPath.mClusterId); } -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) { ChipLogDetail(DataManagement, "Reading attribute: Cluster=" ChipLogFormatMEI " Endpoint=%" PRIx16 " AttributeId=" ChipLogFormatMEI, @@ -232,6 +237,37 @@ CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWr return apWriter->Put(chip::TLV::ContextTag(AttributeDataElement::kCsTag_Status), ToInteractionModelStatus(status)); } +#if 0 + // TODO: get required privilege from yet-to-be-written ember api +#else + // TEMP: assume view privilege required + Privilege privilege = Privilege::View; +#endif + + { + access::RequestPath requestPath { + .endpoint = aPath.mEndpointId, + .cluster = aPath.mClusterId + }; + CHIP_ERROR err = AccessControl::GetInstance()->Check(aSubjectDescriptor, requestPath, privilege); + if (err != CHIP_NO_ERROR) + { + if (err == CHIP_ERROR_ACCESS_DENIED) + { + ChipLogProgress(DataManagement, "ACCESS CONTROL --> DENIED"); + // TODO: In some cases (wildcards) we'll want to just discard request path + return apWriter->Put(chip::TLV::ContextTag(AttributeDataElement::kCsTag_Status), Protocols::InteractionModel::Status::UnsupportedAccess); + } + else + { + return err; + } + } + ChipLogProgress(DataManagement, "ACCESS CONTROL --> ALLOWED"); + } + + // TODO: filter fabric sensitive data if fabric doesn't match + // TODO: ZCL_STRUCT_ATTRIBUTE_TYPE is not included in this switch case currently, should add support for structures. switch (BaseType(attributeType)) { @@ -426,7 +462,8 @@ CHIP_ERROR prepareWriteData(EmberAfAttributeType expectedType, TLV::TLVReader & } } // namespace -static Protocols::InteractionModel::Status WriteSingleClusterDataInternal(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, +static Protocols::InteractionModel::Status WriteSingleClusterDataInternal(const access::SubjectDescriptor & aSubjectDescriptor, + ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler) { // Passing nullptr as buf to emberAfReadAttribute means we only need attribute type here, and ember will not do data read & @@ -439,11 +476,42 @@ static Protocols::InteractionModel::Status WriteSingleClusterDataInternal(Cluste return Protocols::InteractionModel::Status::UnsupportedAttribute; } +#if 0 + // TODO: get required privilege from yet-to-be-written ember api +#else + // TEMP: assume operate privilege required + Privilege privilege = Privilege::Operate; +#endif + + { + access::RequestPath requestPath { + .endpoint = aClusterInfo.mEndpointId, + .cluster = aClusterInfo.mClusterId + }; + CHIP_ERROR err = AccessControl::GetInstance()->Check(aSubjectDescriptor, requestPath, privilege); + if (err != CHIP_NO_ERROR) + { + if (err == CHIP_ERROR_ACCESS_DENIED) + { + ChipLogProgress(DataManagement, "ACCESS CONTROL --> DENIED"); + return Protocols::InteractionModel::Status::UnsupportedAccess; + } + else + { + // TODO: better mapping of chip error to IM status + return Protocols::InteractionModel::Status::Failure; + } + } + ChipLogProgress(DataManagement, "ACCESS CONTROL --> ALLOWED"); + } + + // TODO: don't write fabric scoped data if fabric doesn't match + CHIP_ERROR preparationError = CHIP_NO_ERROR; uint16_t dataLen = 0; if ((preparationError = prepareWriteData(attributeMetadata->attributeType, aReader, dataLen)) != CHIP_NO_ERROR) { - ChipLogDetail(Zcl, "Failed to preapre data to write: %s", ErrorStr(preparationError)); + ChipLogDetail(Zcl, "Failed to prepare data to write: %s", ErrorStr(preparationError)); return Protocols::InteractionModel::Status::InvalidValue; } @@ -458,7 +526,7 @@ static Protocols::InteractionModel::Status WriteSingleClusterDataInternal(Cluste attributeMetadata->attributeType)); } -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler) +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * apWriteHandler) { AttributePathParams attributePathParams; attributePathParams.mNodeId = aClusterInfo.mNodeId; @@ -467,7 +535,7 @@ CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & a attributePathParams.mFieldId = aClusterInfo.mFieldId; attributePathParams.mFlags.Set(AttributePathParams::Flags::kFieldIdValid); - auto imCode = WriteSingleClusterDataInternal(aClusterInfo, aReader, apWriteHandler); + auto imCode = WriteSingleClusterDataInternal(aSubjectDescriptor, aClusterInfo, aReader, apWriteHandler); return apWriteHandler->AddStatus(attributePathParams, imCode); } } // namespace app diff --git a/src/controller/tests/data_model/TestCommands.cpp b/src/controller/tests/data_model/TestCommands.cpp index 5526d0b787eedc..e2dc89b2c24ffc 100644 --- a/src/controller/tests/data_model/TestCommands.cpp +++ b/src/controller/tests/data_model/TestCommands.cpp @@ -120,12 +120,12 @@ bool ServerClusterCommandExists(const ConcreteCommandPath & aCommandPath) return (aCommandPath.mEndpointId == kTestEndpointId && aCommandPath.mClusterId == TestCluster::Id); } -CHIP_ERROR ReadSingleClusterData(const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) +CHIP_ERROR ReadSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, const ConcreteAttributePath & aPath, TLV::TLVWriter * apWriter, bool * apDataExists) { return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; } -CHIP_ERROR WriteSingleClusterData(ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * aWriteHandler) +CHIP_ERROR WriteSingleClusterData(const access::SubjectDescriptor & aSubjectDescriptor, ClusterInfo & aClusterInfo, TLV::TLVReader & aReader, WriteHandler * aWriteHandler) { return CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE; } diff --git a/src/messaging/BUILD.gn b/src/messaging/BUILD.gn index 9a99dd9e5f25a8..7680297ecdf1e9 100644 --- a/src/messaging/BUILD.gn +++ b/src/messaging/BUILD.gn @@ -41,6 +41,7 @@ static_library("messaging") { cflags = [ "-Wconversion" ] public_deps = [ + "${chip_root}/src/access", "${chip_root}/src/crypto", "${chip_root}/src/inet", "${chip_root}/src/lib/core", diff --git a/src/messaging/ExchangeContext.h b/src/messaging/ExchangeContext.h index 71dbadfc86b23a..df5ce045e73cc5 100644 --- a/src/messaging/ExchangeContext.h +++ b/src/messaging/ExchangeContext.h @@ -23,6 +23,7 @@ #pragma once +#include #include #include #include @@ -154,6 +155,25 @@ class DLL_EXPORT ExchangeContext : public ReliableMessageContext, public Referen uint16_t GetExchangeId() const { return mExchangeId; } + const access::SubjectDescriptor GetSubjectDescriptor() const + { + access::SubjectDescriptor subjectDescriptor; + + if (mSecureSession.HasValue()) + { + // TODO: these are just placeholder values for now, need to get appropriate + // fields (auth mode, node id, etc.) from session handle (or whereever) + // and put in SubjectDescriptor appropriately, and ensure it's correct for + // all cases (e.g. peer node ID is good) + auto& session = mSecureSession.Value(); + subjectDescriptor.authMode = access::AuthMode::Case; + subjectDescriptor.subject = session.GetPeerNodeId(); + subjectDescriptor.fabricIndex = session.GetFabricIndex(); + } + + return subjectDescriptor; + } + /* * In order to use reference counting (see refCount below) we use a hold/free paradigm where users of the exchange * can hold onto it while it's out of their direct control to make sure it isn't closed before everyone's ready.