/* * * Copyright (c) 2021 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. */ // Included for the default AccessControlDelegate logging enables/disables. // See `chip_access_control_policy_logging_verbosity` in `src/app/BUILD.gn` for // the levels available. #include #include "AccessControl.h" #include namespace chip { namespace Access { using chip::CATValues; using chip::FabricIndex; using chip::NodeId; namespace { Global defaultAccessControl; AccessControl * globalAccessControl = nullptr; // lazily defaulted to defaultAccessControl in GetAccessControl static_assert(((unsigned(Privilege::kAdminister) & unsigned(Privilege::kManage)) == 0) && ((unsigned(Privilege::kAdminister) & unsigned(Privilege::kOperate)) == 0) && ((unsigned(Privilege::kAdminister) & unsigned(Privilege::kView)) == 0) && ((unsigned(Privilege::kAdminister) & unsigned(Privilege::kProxyView)) == 0) && ((unsigned(Privilege::kManage) & unsigned(Privilege::kOperate)) == 0) && ((unsigned(Privilege::kManage) & unsigned(Privilege::kView)) == 0) && ((unsigned(Privilege::kManage) & unsigned(Privilege::kProxyView)) == 0) && ((unsigned(Privilege::kOperate) & unsigned(Privilege::kView)) == 0) && ((unsigned(Privilege::kOperate) & unsigned(Privilege::kProxyView)) == 0) && ((unsigned(Privilege::kView) & unsigned(Privilege::kProxyView)) == 0), "Privilege bits must be unique"); bool CheckRequestPrivilegeAgainstEntryPrivilege(Privilege requestPrivilege, Privilege entryPrivilege) { switch (entryPrivilege) { case Privilege::kView: return requestPrivilege == Privilege::kView; case Privilege::kProxyView: return requestPrivilege == Privilege::kProxyView || requestPrivilege == Privilege::kView; case Privilege::kOperate: return requestPrivilege == Privilege::kOperate || requestPrivilege == Privilege::kView; case Privilege::kManage: return requestPrivilege == Privilege::kManage || requestPrivilege == Privilege::kOperate || requestPrivilege == Privilege::kView; case Privilege::kAdminister: return requestPrivilege == Privilege::kAdminister || requestPrivilege == Privilege::kManage || requestPrivilege == Privilege::kOperate || requestPrivilege == Privilege::kView || requestPrivilege == Privilege::kProxyView; } return false; } constexpr bool IsValidCaseNodeId(NodeId aNodeId) { if (IsOperationalNodeId(aNodeId)) { return true; } if (IsCASEAuthTag(aNodeId) && (GetCASEAuthTagVersion(CASEAuthTagFromNodeId(aNodeId)) != 0)) { return true; } return false; } constexpr bool IsValidGroupNodeId(NodeId aNodeId) { return IsGroupId(aNodeId) && IsValidGroupId(GroupIdFromNodeId(aNodeId)); } #if CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 char GetAuthModeStringForLogging(AuthMode authMode) { switch (authMode) { case AuthMode::kNone: return 'n'; case AuthMode::kPase: return 'p'; case AuthMode::kCase: return 'c'; case AuthMode::kGroup: return 'g'; } return 'u'; } constexpr int kCharsPerCatForLogging = 11; // including final null terminator char * GetCatStringForLogging(char * buf, size_t size, const CATValues & cats) { if (size == 0) { return nullptr; } char * p = buf; char * const end = buf + size; *p = '\0'; // Format string chars needed: // 1 for comma (optional) // 2 for 0x prefix // 8 for 32-bit hex value // 1 for null terminator (at end) static constexpr char fmtWithoutComma[] = "0x%08" PRIX32; static constexpr char fmtWithComma[] = ",0x%08" PRIX32; constexpr int countWithoutComma = 10; constexpr int countWithComma = countWithoutComma + 1; bool withComma = false; for (auto cat : cats.values) { if (cat == chip::kUndefinedCAT) { break; } snprintf(p, static_cast(end - p), withComma ? fmtWithComma : fmtWithoutComma, cat); p += withComma ? countWithComma : countWithoutComma; if (p >= end) { // Output was truncated. p = end - ((size < 4) ? size : 4); while (*p) { // Indicate truncation if possible. *p++ = '.'; } break; } withComma = true; } return buf; } char GetPrivilegeStringForLogging(Privilege privilege) { switch (privilege) { case Privilege::kView: return 'v'; case Privilege::kProxyView: return 'p'; case Privilege::kOperate: return 'o'; case Privilege::kManage: return 'm'; case Privilege::kAdminister: return 'a'; } return 'u'; } char GetRequestTypeStringForLogging(RequestType requestType) { switch (requestType) { case RequestType::kAttributeReadRequest: return 'r'; case RequestType::kAttributeWriteRequest: return 'w'; case RequestType::kCommandInvokeRequest: return 'i'; case RequestType::kEventReadRequest: return 'e'; default: return '?'; } } #endif // CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 } // namespace Global AccessControl::Entry::mDefaultDelegate; Global AccessControl::EntryIterator::mDefaultDelegate; CHIP_ERROR AccessControl::Init(AccessControl::Delegate * delegate, DeviceTypeResolver & deviceTypeResolver) { VerifyOrReturnError(!IsInitialized(), CHIP_ERROR_INCORRECT_STATE); ChipLogProgress(DataManagement, "AccessControl: initializing"); VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INVALID_ARGUMENT); CHIP_ERROR retval = delegate->Init(); if (retval == CHIP_NO_ERROR) { mDelegate = delegate; mDeviceTypeResolver = &deviceTypeResolver; } return retval; } void AccessControl::Finish() { VerifyOrReturn(IsInitialized()); ChipLogProgress(DataManagement, "AccessControl: finishing"); mDelegate->Finish(); mDelegate = nullptr; } CHIP_ERROR AccessControl::CreateEntry(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t * index, const Entry & entry) { VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE); size_t count = 0; size_t maxCount = 0; ReturnErrorOnFailure(mDelegate->GetEntryCount(fabric, count)); ReturnErrorOnFailure(mDelegate->GetMaxEntriesPerFabric(maxCount)); VerifyOrReturnError((count + 1) <= maxCount, CHIP_ERROR_BUFFER_TOO_SMALL); VerifyOrReturnError(IsValid(entry), CHIP_ERROR_INVALID_ARGUMENT); size_t i = 0; ReturnErrorOnFailure(mDelegate->CreateEntry(&i, entry, &fabric)); if (index) { *index = i; } NotifyEntryChanged(subjectDescriptor, fabric, i, &entry, EntryListener::ChangeType::kAdded); return CHIP_NO_ERROR; } CHIP_ERROR AccessControl::UpdateEntry(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, const Entry & entry) { VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE); VerifyOrReturnError(IsValid(entry), CHIP_ERROR_INVALID_ARGUMENT); ReturnErrorOnFailure(mDelegate->UpdateEntry(index, entry, &fabric)); NotifyEntryChanged(subjectDescriptor, fabric, index, &entry, EntryListener::ChangeType::kUpdated); return CHIP_NO_ERROR; } CHIP_ERROR AccessControl::DeleteEntry(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index) { VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE); Entry entry; Entry * p = nullptr; if (mEntryListener != nullptr && ReadEntry(fabric, index, entry) == CHIP_NO_ERROR) { p = &entry; } ReturnErrorOnFailure(mDelegate->DeleteEntry(index, &fabric)); if (p && p->HasDefaultDelegate()) { // The entry was read prior to deletion so its latest value could be provided // to the listener after deletion. If it's been reset to its default delegate, // that best effort attempt to retain the latest value failed. This is // regrettable but OK. p = nullptr; } NotifyEntryChanged(subjectDescriptor, fabric, index, p, EntryListener::ChangeType::kRemoved); return CHIP_NO_ERROR; } void AccessControl::AddEntryListener(EntryListener & listener) { if (mEntryListener == nullptr) { mEntryListener = &listener; listener.mNext = nullptr; return; } for (EntryListener * l = mEntryListener; /**/; l = l->mNext) { if (l == &listener) { return; } if (l->mNext == nullptr) { l->mNext = &listener; listener.mNext = nullptr; return; } } } void AccessControl::RemoveEntryListener(EntryListener & listener) { if (mEntryListener == &listener) { mEntryListener = listener.mNext; listener.mNext = nullptr; return; } for (EntryListener * l = mEntryListener; l != nullptr; l = l->mNext) { if (l->mNext == &listener) { l->mNext = listener.mNext; listener.mNext = nullptr; return; } } } bool AccessControl::IsAccessRestrictionListSupported() const { #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS return mAccessRestrictionProvider != nullptr; #else return false; #endif } CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege requestPrivilege) { VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE); CHIP_ERROR result = CheckACL(subjectDescriptor, requestPath, requestPrivilege); #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS if (result == CHIP_NO_ERROR) { result = CheckARL(subjectDescriptor, requestPath, requestPrivilege); } #endif return result; } CHIP_ERROR AccessControl::CheckACL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege requestPrivilege) { #if CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 { constexpr size_t kMaxCatsToLog = 6; char catLogBuf[kMaxCatsToLog * kCharsPerCatForLogging]; ChipLogProgress(DataManagement, "AccessControl: checking f=%u a=%c s=0x" ChipLogFormatX64 " t=%s c=" ChipLogFormatMEI " e=%u p=%c r=%c", subjectDescriptor.fabricIndex, GetAuthModeStringForLogging(subjectDescriptor.authMode), ChipLogValueX64(subjectDescriptor.subject), GetCatStringForLogging(catLogBuf, sizeof(catLogBuf), subjectDescriptor.cats), ChipLogValueMEI(requestPath.cluster), requestPath.endpoint, GetPrivilegeStringForLogging(requestPrivilege), GetRequestTypeStringForLogging(requestPath.requestType)); } #endif // CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 { CHIP_ERROR result = mDelegate->Check(subjectDescriptor, requestPath, requestPrivilege); if (result != CHIP_ERROR_NOT_IMPLEMENTED) { #if CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 0 ChipLogProgress(DataManagement, "AccessControl: %s (delegate)", (result == CHIP_NO_ERROR) ? "allowed" : (result == CHIP_ERROR_ACCESS_DENIED) ? "denied" : "error"); #else if (result != CHIP_NO_ERROR) { ChipLogProgress(DataManagement, "AccessControl: %s (delegate)", (result == CHIP_ERROR_ACCESS_DENIED) ? "denied" : "error"); } #endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 0 return result; } } // Operational PASE not supported for v1.0, so PASE implies commissioning, which has highest privilege. // Currently, subject descriptor is only PASE if this node is the responder (aka commissionee); // if this node is the initiator (aka commissioner) then the subject descriptor remains blank. if (subjectDescriptor.authMode == AuthMode::kPase) { #if CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 ChipLogProgress(DataManagement, "AccessControl: implicit admin (PASE)"); #endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 return CHIP_NO_ERROR; } EntryIterator iterator; ReturnErrorOnFailure(Entries(iterator, &subjectDescriptor.fabricIndex)); Entry entry; while (iterator.Next(entry) == CHIP_NO_ERROR) { AuthMode authMode = AuthMode::kNone; ReturnErrorOnFailure(entry.GetAuthMode(authMode)); // Operational PASE not supported for v1.0. VerifyOrReturnError(authMode == AuthMode::kCase || authMode == AuthMode::kGroup, CHIP_ERROR_INCORRECT_STATE); if (authMode != subjectDescriptor.authMode) { continue; } Privilege privilege = Privilege::kView; ReturnErrorOnFailure(entry.GetPrivilege(privilege)); if (!CheckRequestPrivilegeAgainstEntryPrivilege(requestPrivilege, privilege)) { continue; } size_t subjectCount = 0; ReturnErrorOnFailure(entry.GetSubjectCount(subjectCount)); if (subjectCount > 0) { bool subjectMatched = false; for (size_t i = 0; i < subjectCount; ++i) { NodeId subject = kUndefinedNodeId; ReturnErrorOnFailure(entry.GetSubject(i, subject)); if (IsOperationalNodeId(subject)) { VerifyOrReturnError(authMode == AuthMode::kCase, CHIP_ERROR_INCORRECT_STATE); if (subject == subjectDescriptor.subject) { subjectMatched = true; break; } } else if (IsCASEAuthTag(subject)) { VerifyOrReturnError(authMode == AuthMode::kCase, CHIP_ERROR_INCORRECT_STATE); if (subjectDescriptor.cats.CheckSubjectAgainstCATs(subject)) { subjectMatched = true; break; } } else if (IsGroupId(subject)) { VerifyOrReturnError(authMode == AuthMode::kGroup, CHIP_ERROR_INCORRECT_STATE); if (subject == subjectDescriptor.subject) { subjectMatched = true; break; } } else { // Operational PASE not supported for v1.0. return CHIP_ERROR_INCORRECT_STATE; } } if (!subjectMatched) { continue; } } size_t targetCount = 0; ReturnErrorOnFailure(entry.GetTargetCount(targetCount)); if (targetCount > 0) { bool targetMatched = false; for (size_t i = 0; i < targetCount; ++i) { Entry::Target target; ReturnErrorOnFailure(entry.GetTarget(i, target)); if ((target.flags & Entry::Target::kCluster) && target.cluster != requestPath.cluster) { continue; } if ((target.flags & Entry::Target::kEndpoint) && target.endpoint != requestPath.endpoint) { continue; } if (target.flags & Entry::Target::kDeviceType && !mDeviceTypeResolver->IsDeviceTypeOnEndpoint(target.deviceType, requestPath.endpoint)) { continue; } targetMatched = true; break; } if (!targetMatched) { continue; } } // Entry passed all checks: access is allowed. #if CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 0 ChipLogProgress(DataManagement, "AccessControl: allowed"); #endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 0 return CHIP_NO_ERROR; } // No entry was found which passed all checks: access is denied. ChipLogProgress(DataManagement, "AccessControl: denied"); return CHIP_ERROR_ACCESS_DENIED; } #if CHIP_CONFIG_USE_ACCESS_RESTRICTIONS CHIP_ERROR AccessControl::CheckARL(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege requestPrivilege) { CHIP_ERROR result = CHIP_NO_ERROR; VerifyOrReturnError(requestPath.requestType != RequestType::kRequestTypeUnknown, CHIP_ERROR_INVALID_ARGUMENT); if (!IsAccessRestrictionListSupported()) { // Access Restriction support is compiled in, but not configured/enabled. Nothing to restrict. return CHIP_NO_ERROR; } if (subjectDescriptor.isCommissioning) { result = mAccessRestrictionProvider->CheckForCommissioning(subjectDescriptor, requestPath); } else { result = mAccessRestrictionProvider->Check(subjectDescriptor, requestPath); } if (result != CHIP_NO_ERROR) { ChipLogProgress(DataManagement, "AccessControl: %s", (result == CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL) ? "denied (restricted)" : "denied (restriction error)"); return result; } return result; } #endif #if CHIP_ACCESS_CONTROL_DUMP_ENABLED CHIP_ERROR AccessControl::Dump(const Entry & entry) { CHIP_ERROR err; ChipLogDetail(DataManagement, "----- BEGIN ENTRY -----"); { FabricIndex fabricIndex; SuccessOrExit(err = entry.GetFabricIndex(fabricIndex)); ChipLogDetail(DataManagement, "fabricIndex: %u", fabricIndex); } { Privilege privilege; SuccessOrExit(err = entry.GetPrivilege(privilege)); ChipLogDetail(DataManagement, "privilege: %d", to_underlying(privilege)); } { AuthMode authMode; SuccessOrExit(err = entry.GetAuthMode(authMode)); ChipLogDetail(DataManagement, "authMode: %d", to_underlying(authMode)); } { size_t count; SuccessOrExit(err = entry.GetSubjectCount(count)); if (count) { ChipLogDetail(DataManagement, "subjects: %u", static_cast(count)); for (size_t i = 0; i < count; ++i) { NodeId subject; SuccessOrExit(err = entry.GetSubject(i, subject)); ChipLogDetail(DataManagement, " %u: 0x" ChipLogFormatX64, static_cast(i), ChipLogValueX64(subject)); } } } { size_t count; SuccessOrExit(err = entry.GetTargetCount(count)); if (count) { ChipLogDetail(DataManagement, "targets: %u", static_cast(count)); for (size_t i = 0; i < count; ++i) { Entry::Target target; SuccessOrExit(err = entry.GetTarget(i, target)); if (target.flags & Entry::Target::kCluster) { ChipLogDetail(DataManagement, " %u: cluster: 0x" ChipLogFormatMEI, static_cast(i), ChipLogValueMEI(target.cluster)); } if (target.flags & Entry::Target::kEndpoint) { ChipLogDetail(DataManagement, " %u: endpoint: %u", static_cast(i), target.endpoint); } if (target.flags & Entry::Target::kDeviceType) { ChipLogDetail(DataManagement, " %u: deviceType: 0x" ChipLogFormatMEI, static_cast(i), ChipLogValueMEI(target.deviceType)); } } } } ChipLogDetail(DataManagement, "----- END ENTRY -----"); return CHIP_NO_ERROR; exit: ChipLogError(DataManagement, "AccessControl: dump failed %" CHIP_ERROR_FORMAT, err.Format()); return err; } #endif bool AccessControl::IsValid(const Entry & entry) { const char * log = "unexpected error"; IgnoreUnusedVariable(log); // logging may be disabled AuthMode authMode = AuthMode::kNone; FabricIndex fabricIndex = kUndefinedFabricIndex; Privilege privilege = static_cast(0); size_t subjectCount = 0; size_t targetCount = 0; CHIP_ERROR err = CHIP_NO_ERROR; SuccessOrExit(err = entry.GetAuthMode(authMode)); SuccessOrExit(err = entry.GetFabricIndex(fabricIndex)); SuccessOrExit(err = entry.GetPrivilege(privilege)); SuccessOrExit(err = entry.GetSubjectCount(subjectCount)); SuccessOrExit(err = entry.GetTargetCount(targetCount)); #if CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 ChipLogProgress(DataManagement, "AccessControl: validating f=%u p=%c a=%c s=%d t=%d", fabricIndex, GetPrivilegeStringForLogging(privilege), GetAuthModeStringForLogging(authMode), static_cast(subjectCount), static_cast(targetCount)); #endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 // Fabric index must be defined. VerifyOrExit(fabricIndex != kUndefinedFabricIndex, log = "invalid fabric index"); if (authMode != AuthMode::kCase) { // Operational PASE not supported for v1.0 (so must be group). VerifyOrExit(authMode == AuthMode::kGroup, log = "invalid auth mode"); // Privilege must not be administer. VerifyOrExit(privilege != Privilege::kAdminister, log = "invalid privilege"); } for (size_t i = 0; i < subjectCount; ++i) { NodeId subject; SuccessOrExit(err = entry.GetSubject(i, subject)); const bool kIsCase = authMode == AuthMode::kCase; const bool kIsGroup = authMode == AuthMode::kGroup; #if CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 ChipLogProgress(DataManagement, " validating subject 0x" ChipLogFormatX64, ChipLogValueX64(subject)); #endif // CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1 VerifyOrExit((kIsCase && IsValidCaseNodeId(subject)) || (kIsGroup && IsValidGroupNodeId(subject)), log = "invalid subject"); } for (size_t i = 0; i < targetCount; ++i) { Entry::Target target; SuccessOrExit(err = entry.GetTarget(i, target)); const bool kHasCluster = target.flags & Entry::Target::kCluster; const bool kHasEndpoint = target.flags & Entry::Target::kEndpoint; const bool kHasDeviceType = target.flags & Entry::Target::kDeviceType; VerifyOrExit((kHasCluster || kHasEndpoint || kHasDeviceType) && !(kHasEndpoint && kHasDeviceType) && (!kHasCluster || IsValidClusterId(target.cluster)) && (!kHasEndpoint || IsValidEndpointId(target.endpoint)) && (!kHasDeviceType || IsValidDeviceTypeId(target.deviceType)), log = "invalid target"); } return true; exit: if (err != CHIP_NO_ERROR) { ChipLogError(DataManagement, "AccessControl: %s %" CHIP_ERROR_FORMAT, log, err.Format()); } else { ChipLogError(DataManagement, "AccessControl: %s", log); } return false; } void AccessControl::NotifyEntryChanged(const SubjectDescriptor * subjectDescriptor, FabricIndex fabric, size_t index, const Entry * entry, EntryListener::ChangeType changeType) { for (EntryListener * listener = mEntryListener; listener != nullptr; listener = listener->mNext) { listener->OnEntryChanged(subjectDescriptor, fabric, index, entry, changeType); } } AccessControl & GetAccessControl() { return (globalAccessControl) ? *globalAccessControl : defaultAccessControl.get(); } void SetAccessControl(AccessControl & accessControl) { ChipLogProgress(DataManagement, "AccessControl: setting"); globalAccessControl = &accessControl; } void ResetAccessControlToDefault() { globalAccessControl = nullptr; } } // namespace Access } // namespace chip