blob: 88d7d34cb811093fc56d4472352164255f904e07 [file] [log] [blame]
/*
*
* 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 <app/AppConfig.h>
#include "AccessControl.h"
#include <lib/core/Global.h>
namespace chip {
namespace Access {
using chip::CATValues;
using chip::FabricIndex;
using chip::NodeId;
namespace {
Global<AccessControl> 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)
constexpr char fmtWithoutComma[] = "0x%08" PRIX32;
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<size_t>(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';
}
#endif // CHIP_PROGRESS_LOGGING && CHIP_CONFIG_ACCESS_CONTROL_POLICY_LOGGING_VERBOSITY > 1
} // namespace
Global<AccessControl::Entry::Delegate> AccessControl::Entry::mDefaultDelegate;
Global<AccessControl::EntryIterator::Delegate> 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);
ReturnErrorCodeIf(!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);
ReturnErrorCodeIf(!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;
}
}
}
CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath,
Privilege requestPrivilege)
{
VerifyOrReturnError(IsInitialized(), CHIP_ERROR_INCORRECT_STATE);
#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",
subjectDescriptor.fabricIndex, GetAuthModeStringForLogging(subjectDescriptor.authMode),
ChipLogValueX64(subjectDescriptor.subject),
GetCatStringForLogging(catLogBuf, sizeof(catLogBuf), subjectDescriptor.cats),
ChipLogValueMEI(requestPath.cluster), requestPath.endpoint, GetPrivilegeStringForLogging(requestPrivilege));
}
#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_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<unsigned>(count));
for (size_t i = 0; i < count; ++i)
{
NodeId subject;
SuccessOrExit(err = entry.GetSubject(i, subject));
ChipLogDetail(DataManagement, " %u: 0x" ChipLogFormatX64, static_cast<unsigned>(i), ChipLogValueX64(subject));
}
}
}
{
size_t count;
SuccessOrExit(err = entry.GetTargetCount(count));
if (count)
{
ChipLogDetail(DataManagement, "targets: %u", static_cast<unsigned>(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<unsigned>(i),
ChipLogValueMEI(target.cluster));
}
if (target.flags & Entry::Target::kEndpoint)
{
ChipLogDetail(DataManagement, " %u: endpoint: %u", static_cast<unsigned>(i), target.endpoint);
}
if (target.flags & Entry::Target::kDeviceType)
{
ChipLogDetail(DataManagement, " %u: deviceType: 0x" ChipLogFormatMEI, static_cast<unsigned>(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<Privilege>(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<int>(subjectCount),
static_cast<int>(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