From 15142006088a268ed3f5d9d9fc5de514b054fec9 Mon Sep 17 00:00:00 2001 From: Marc Lepage <67919234+mlepage-google@users.noreply.github.com> Date: Wed, 27 Oct 2021 18:03:28 -0400 Subject: [PATCH] Add initial prototype of AccessControl module (#10579) * Add initial prototype of AccessControl module - Not complete, always allows actions - Not hooked up to interaction model or messaging layer - Progress toward issues #10236 and #10249 - Fully isolated as a module - Has unit tests * Remove file comments from files * Add 'k' prefix to enum values * Restyled by whitespace * Restyled by clang-format * Restyled by gn * Remove "empty" .cpp files * Apply suggestions from code review * Apply suggestions from code review * Fix compatibility under different compilers * Fix unit test compatability on different compilers * Restyled by clang-format * Change forward declaration to include Allows tooling to detect circular dependencies. * Changes from code review suggestions - rename namespace access --> Access - rename DataProvider --> AccessControlDataProvider - decouple DataProvider lifecycle (Init/Finish) - rename DataProviderImpl --> ExampleAccessControlDataProvider - change GetInstance/SetInstance to global functions - remove Config.h since global instance must be set - change EntryIterator::Next to return pointer - add comments to Privilege and AuthMode - remove SubjectDescriptor.isCommissioning for now - improve naming of CAT subjects in SubjectDescriptor - change SubjectId typedef to use NodeId * Make tests table-driven Should also fix some build complaints on ESP32 * Restyled by clang-format * Restyle * Add more metatesting Ensure not just that results are correct, but that they were correctly obtained. * Restyled by clang-format * Change enums to enum classes * Address review comments * Use basic types in lib/core Basic types (FabricIndex etc.) were moved in PR #10925 from app to lib/core, so now they can be used from this module. * A bit of cleanup * Refactor SubjectId and SubjectDescriptor Also, remove CatId and move PasscodeId into lib/core. * Restyled by clang-format * Add clarifying examples to documentation. Co-authored-by: Restyled.io --- src/BUILD.gn | 3 +- src/access/AccessControl.cpp | 115 ++++ src/access/AccessControl.h | 90 +++ src/access/AccessControlDataProvider.h | 134 +++++ src/access/AuthMode.h | 35 ++ src/access/BUILD.gn | 39 ++ src/access/Privilege.h | 37 ++ src/access/RequestPath.h | 34 ++ src/access/SubjectDescriptor.h | 54 ++ .../ExampleAccessControlDataProvider.cpp | 51 ++ .../ExampleAccessControlDataProvider.h | 29 + src/access/tests/BUILD.gn | 32 + src/access/tests/TestAccessControl.cpp | 555 ++++++++++++++++++ src/lib/core/BUILD.gn | 1 + src/lib/core/PasscodeId.h | 30 + 15 files changed, 1238 insertions(+), 1 deletion(-) create mode 100644 src/access/AccessControl.cpp create mode 100644 src/access/AccessControl.h create mode 100644 src/access/AccessControlDataProvider.h create mode 100644 src/access/AuthMode.h create mode 100644 src/access/BUILD.gn create mode 100644 src/access/Privilege.h create mode 100644 src/access/RequestPath.h create mode 100644 src/access/SubjectDescriptor.h create mode 100644 src/access/examples/ExampleAccessControlDataProvider.cpp create mode 100644 src/access/examples/ExampleAccessControlDataProvider.h create mode 100644 src/access/tests/BUILD.gn create mode 100644 src/access/tests/TestAccessControl.cpp create mode 100644 src/lib/core/PasscodeId.h diff --git a/src/BUILD.gn b/src/BUILD.gn index c3353cd3a77d53..3c226c5fdb61de 100644 --- a/src/BUILD.gn +++ b/src/BUILD.gn @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Project CHIP Authors +# Copyright (c) 2020-2021 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. @@ -44,6 +44,7 @@ if (chip_build_tests) { chip_test_group("tests") { deps = [ + "${chip_root}/src/access/tests", "${chip_root}/src/app/tests", "${chip_root}/src/crypto/tests", "${chip_root}/src/inet/tests", diff --git a/src/access/AccessControl.cpp b/src/access/AccessControl.cpp new file mode 100644 index 00000000000000..ca062f28e2927e --- /dev/null +++ b/src/access/AccessControl.cpp @@ -0,0 +1,115 @@ +/* + * + * 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. + */ + +#include "AccessControl.h" + +namespace { + +using chip::FabricIndex; +using namespace chip::Access; + +// Avoid GetAccessControl returning nullptr before SetAccessControl is called. +class UnimplementedDataProvider : public AccessControlDataProvider +{ + CHIP_ERROR Init() override { return CHIP_NO_ERROR; } + + void Finish() override {} + + EntryIterator * Entries() const override { return nullptr; } + + EntryIterator * Entries(FabricIndex fabricIndex) const override { return nullptr; } +}; + +// Avoid GetAccessControl returning nullptr before SetAccessControl is called. +UnimplementedDataProvider gUnimplementedDataProvider; +AccessControl gUnimplementedAccessControl(gUnimplementedDataProvider); + +AccessControl * gAccessControl = &gUnimplementedAccessControl; + +} // namespace + +namespace chip { +namespace Access { + +CHIP_ERROR AccessControl::Init() +{ + ChipLogDetail(DataManagement, "access control: initializing"); + // ... + return CHIP_NO_ERROR; +} + +void AccessControl::Finish() +{ + ChipLogDetail(DataManagement, "access control: finishing"); + // ... +} + +CHIP_ERROR AccessControl::Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege privilege) +{ + CHIP_ERROR err = CHIP_ERROR_ACCESS_DENIED; + + EntryIterator * iterator = mDataProvider.Entries(subjectDescriptor.fabricIndex); + // TODO: check error (but can't until we have an implementation) +#if 0 + ReturnErrorCodeIf(iterator == nullptr, CHIP_ERROR_INTERNAL); +#else + ReturnErrorCodeIf(iterator == nullptr, CHIP_NO_ERROR); +#endif + + // TODO: a few more cases (PASE commissioning, CASE Authenticated Tags, etc.) + + while (auto entry = iterator->Next()) + { + ChipLogDetail(DataManagement, "Checking entry"); + + if (!entry->MatchesPrivilege(privilege)) + continue; + ChipLogDetail(DataManagement, " --> matched privilege"); + if (!entry->MatchesAuthMode(subjectDescriptor.authMode)) + continue; + ChipLogDetail(DataManagement, " --> matched authmode"); + if (!entry->MatchesSubject(subjectDescriptor.subjects[0])) + continue; + ChipLogDetail(DataManagement, " --> matched subject"); + if (!entry->MatchesTarget(requestPath.endpoint, requestPath.cluster)) + continue; + ChipLogDetail(DataManagement, " --> matched target"); + + err = CHIP_NO_ERROR; + break; + } + + iterator->Release(); + return err; +} + +AccessControl * GetAccessControl() +{ + return gAccessControl; +} + +void SetAccessControl(AccessControl * accessControl) +{ + if (accessControl != nullptr) + { + gAccessControl = accessControl; + } +} + +} // namespace Access +} // namespace chip diff --git a/src/access/AccessControl.h b/src/access/AccessControl.h new file mode 100644 index 00000000000000..554937e43356dd --- /dev/null +++ b/src/access/AccessControl.h @@ -0,0 +1,90 @@ +/* + * + * 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. + */ + +#pragma once + +#include "AccessControlDataProvider.h" +#include "Privilege.h" +#include "RequestPath.h" +#include "SubjectDescriptor.h" + +#include + +namespace chip { +namespace Access { + +class AccessControl +{ +public: + /** + * Create an access control module. Must be initialized before use, and + * deinitialized when finished. Must be configured with an + * AccessControlDataProvider, which must outlive this module. + */ + AccessControl(AccessControlDataProvider & dataProvider) : mDataProvider(dataProvider) {} + + AccessControl(const AccessControl &) = delete; + AccessControl & operator=(const AccessControl &) = delete; + + /** + * Initialize the access control module. Must be called before first use. + * + * @retval various errors, probably fatal. + */ + CHIP_ERROR Init(); + + /** + * Deinitialize the access control module. Must be called when finished. + */ + void Finish(); + + /** + * Check whether access (by a subject descriptor, to a request path, + * requiring a privilege) should be allowed or denied. + * + * @retval #CHIP_ERROR_ACCESS_DENIED if denied. + * @retval other errors should also be treated as denied. + * @retval #CHIP_NO_ERROR if allowed. + */ + CHIP_ERROR Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath, Privilege privilege); + +private: + AccessControlDataProvider & mDataProvider; +}; + +/** + * Instance getter for the global AccessControl. + * + * Callers have to externally synchronize usage of this function. + * + * @return The global AccessControl instance. Assume never null. + */ +AccessControl * GetAccessControl(); + +/** + * Instance setter for the global AccessControl. + * + * Callers have to externally synchronize usage of this function. + * + * @param[in] accessControl the instance to start returning with the getter; + * if nullptr, no change occurs. + */ +void SetAccessControl(AccessControl * accessControl); + +} // namespace Access +} // namespace chip diff --git a/src/access/AccessControlDataProvider.h b/src/access/AccessControlDataProvider.h new file mode 100644 index 00000000000000..eb693f80e3b1d6 --- /dev/null +++ b/src/access/AccessControlDataProvider.h @@ -0,0 +1,134 @@ +/* + * + * 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. + */ + +#pragma once + +#include "AuthMode.h" +#include "Privilege.h" +#include "SubjectDescriptor.h" + +#include +#include + +namespace chip { +namespace Access { + +class Entry +{ +public: + virtual ~Entry() = default; + + /** + * Whether the auth mode matches the entry. Must be called before calling + * MatchesSubject. + */ + virtual bool MatchesAuthMode(AuthMode authMode) const = 0; + + /** + * Whether the fabric matches the entry. Entries with fabric index 0 will + * match all fabrics. + */ + virtual bool MatchesFabric(FabricIndex fabricIndex) const = 0; + + /** + * Whether the privilege matches the entry, including subsumed privileges. + * E.g. both Privilege::kOperate and Privilege::kView will match an entry + * with Privilege::kOperate, but Privilege::kManage will not match such an + * entry. + */ + virtual bool MatchesPrivilege(Privilege privilege) const = 0; + + /** + * Whether the subject matches the entry. Must only be called if auth mode + * matches. + */ + virtual bool MatchesSubject(SubjectId subject) const = 0; + + /** + * Whether the target matches the entry. Some entries may match all + * endpoints or all clusters. + */ + virtual bool MatchesTarget(EndpointId endpoint, ClusterId cluster) const = 0; +}; + +class EntryIterator +{ +public: + /** + * Create an entry iterator. Must call release when finished. + */ + EntryIterator() = default; + + virtual ~EntryIterator() = default; + + /** + * Returns the next entry, or nullptr if there is no next entry. + */ + virtual Entry * Next() = 0; + + /** + * Release the iterator. Must be called when finished. + */ + virtual void Release() = 0; +}; + +class AccessControlDataProvider +{ +public: + /** + * Create a data provider. Must be initialized before use, and deinitialized + * when finished. + */ + AccessControlDataProvider() = default; + + virtual ~AccessControlDataProvider() = default; + + AccessControlDataProvider(const AccessControlDataProvider &) = delete; + AccessControlDataProvider & operator=(const AccessControlDataProvider &) = delete; + + /** + * Initialize the data provider. + * + * @retval various errors, probably fatal. + */ + virtual CHIP_ERROR Init() = 0; + + /** + * Deinitialize the data provider. + */ + virtual void Finish() = 0; + + /** + * Get an iterator over all entries. + * + * @retval iterator, release when finished. + * @retval nullptr if error, probably fatal, generally should not happen. + */ + virtual EntryIterator * Entries() const = 0; + + /** + * Get an iterator over all entries for a particular fabric. + * + * @retval iterator, release when finished. + * @retval nullptr if error, probably fatal, generally should not happen. + */ + virtual EntryIterator * Entries(FabricIndex fabricIndex) const = 0; +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/AuthMode.h b/src/access/AuthMode.h new file mode 100644 index 00000000000000..75a667afd07f5e --- /dev/null +++ b/src/access/AuthMode.h @@ -0,0 +1,35 @@ +/* + * + * 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. + */ + +#pragma once + +namespace chip { +namespace Access { + +// Using bitfield values so auth mode and privilege set can be stored together. +// Auth mode should have only one value expressed, which should not be None. +enum class AuthMode +{ + kNone = 0, + kPase = 1 << 5, + kCase = 1 << 6, + kGroup = 1 << 7 +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/BUILD.gn b/src/access/BUILD.gn new file mode 100644 index 00000000000000..25836f2e472c8f --- /dev/null +++ b/src/access/BUILD.gn @@ -0,0 +1,39 @@ +# Copyright (c) 2021 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. +# 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. + +import("//build_overrides/chip.gni") + +static_library("access") { + output_name = "libaccess" + + sources = [ + "AccessControl.cpp", + "AccessControl.h", + "AccessControlDataProvider.h", + "AuthMode.h", + "Privilege.h", + "RequestPath.h", + "SubjectDescriptor.h", + "examples/ExampleAccessControlDataProvider.cpp", + "examples/ExampleAccessControlDataProvider.h", + ] + + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/src/lib/core", + "${chip_root}/src/lib/support", + "${chip_root}/src/platform", + ] +} diff --git a/src/access/Privilege.h b/src/access/Privilege.h new file mode 100644 index 00000000000000..9aacecad99e911 --- /dev/null +++ b/src/access/Privilege.h @@ -0,0 +1,37 @@ +/* + * + * 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. + */ + +#pragma once + +namespace chip { +namespace Access { + +// Using bitfield values so privilege set and auth mode can be stored together. +// Privilege set can have more than one value expressed (e.g. View, +// ProxyView, and Operate). +enum class Privilege +{ + kView = 1 << 0, + kProxyView = 1 << 1, + kOperate = 1 << 2, + kManage = 1 << 3, + kAdminister = 1 << 4 +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/RequestPath.h b/src/access/RequestPath.h new file mode 100644 index 00000000000000..09a180bff957ae --- /dev/null +++ b/src/access/RequestPath.h @@ -0,0 +1,34 @@ +/* + * + * 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. + */ + +#pragma once + +#include + +namespace chip { +namespace Access { + +struct RequestPath +{ + // NOTE: eventually this will likely also contain node, for proxying + EndpointId endpoint = 0; + ClusterId cluster = 0; +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/SubjectDescriptor.h b/src/access/SubjectDescriptor.h new file mode 100644 index 00000000000000..24502ed065c4bb --- /dev/null +++ b/src/access/SubjectDescriptor.h @@ -0,0 +1,54 @@ +/* + * + * 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. + */ + +#pragma once + +#include "AuthMode.h" + +#include +#include +#include +#include + +namespace chip { +namespace Access { + +union SubjectId +{ + PasscodeId passcode; + NodeId node; + GroupId group; +}; + +struct SubjectDescriptor +{ + // Holds FabricIndex of fabric, 0 if no fabric. + FabricIndex fabricIndex = 0; + + // Holds AuthMode of subject(s), kNone if no access. + AuthMode authMode = AuthMode::kNone; + + // NOTE: due to packing there should be free bytes here + + // Holds subjects according to auth mode, and the latter two are only valid + // if auth mode is CASE. + SubjectId subjects[3] = {}; +}; + +} // namespace Access +} // namespace chip diff --git a/src/access/examples/ExampleAccessControlDataProvider.cpp b/src/access/examples/ExampleAccessControlDataProvider.cpp new file mode 100644 index 00000000000000..c5c2a27f9a20aa --- /dev/null +++ b/src/access/examples/ExampleAccessControlDataProvider.cpp @@ -0,0 +1,51 @@ +/* + * + * 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. + */ + +#include "ExampleAccessControlDataProvider.h" + +namespace { + +using chip::FabricIndex; +using namespace chip::Access; + +class ExampleDataProvider : public AccessControlDataProvider +{ + CHIP_ERROR Init() override { return CHIP_NO_ERROR; } + + void Finish() override {} + + EntryIterator * Entries() const override { return nullptr; } + + EntryIterator * Entries(FabricIndex fabricIndex) const override { return nullptr; } +}; + +} // namespace + +namespace chip { +namespace Access { +namespace Examples { + +AccessControlDataProvider * GetExampleAccessControlDataProvider() +{ + static ExampleDataProvider exampleProvider; + return &exampleProvider; +} + +} // namespace Examples +} // namespace Access +} // namespace chip diff --git a/src/access/examples/ExampleAccessControlDataProvider.h b/src/access/examples/ExampleAccessControlDataProvider.h new file mode 100644 index 00000000000000..f679ef513ef204 --- /dev/null +++ b/src/access/examples/ExampleAccessControlDataProvider.h @@ -0,0 +1,29 @@ +/* + * + * Copyright (c) 2021 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. + * 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 "access/AccessControlDataProvider.h" + +namespace chip { +namespace Access { +namespace Examples { + +AccessControlDataProvider * GetExampleAccessControlDataProvider(); + +} // namespace Examples +} // namespace Access +} // namespace chip diff --git a/src/access/tests/BUILD.gn b/src/access/tests/BUILD.gn new file mode 100644 index 00000000000000..c6986e5934c9b3 --- /dev/null +++ b/src/access/tests/BUILD.gn @@ -0,0 +1,32 @@ +# Copyright (c) 2021 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. +# 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. + +import("//build_overrides/build.gni") +import("//build_overrides/chip.gni") +import("//build_overrides/nlunit_test.gni") + +import("${chip_root}/build/chip/chip_test_suite.gni") + +chip_test_suite("tests") { + output_name = "libaccesstest" + + test_sources = [ "TestAccessControl.cpp" ] + + cflags = [ "-Wconversion" ] + + public_deps = [ + "${chip_root}/src/access", + "${nlunit_test_root}:nlunit-test", + ] +} diff --git a/src/access/tests/TestAccessControl.cpp b/src/access/tests/TestAccessControl.cpp new file mode 100644 index 00000000000000..aea90be12e488c --- /dev/null +++ b/src/access/tests/TestAccessControl.cpp @@ -0,0 +1,555 @@ +/* + * + * 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. + */ + +#include "access/AccessControl.h" + +#include +#include + +#include + +namespace { + +using namespace chip; +using namespace chip::Access; + +constexpr EndpointId kEndpoint0 = 0; +constexpr EndpointId kEndpoint1 = 1; +constexpr EndpointId kEndpoint2 = 2; + +constexpr ClusterId kOnOffCluster = 0x00000006; +constexpr ClusterId kLevelControlCluster = 0x00000008; +constexpr ClusterId kColorControlCluster = 0x00000300; +constexpr ClusterId kAccessControlCluster = 0x0000001F; + +constexpr size_t kSubjectsPerEntry = 4; +constexpr size_t kTargetsPerEntry = 3; + +// Used to detect empty subjects, targets, etc. +constexpr int kEmptyFlags = 0; + +// For test purposes, store all subjects as node, use tags to discriminate +// passcode/group, and don't allow 0. +constexpr NodeId kTestSubjectMask = 0xFFFFFFFFFFFF0000; +constexpr NodeId kTestSubjectPasscode = 0xDDDDDDDDDDDD0000; +constexpr NodeId kTestSubjectGroup = 0xEEEEEEEEEEEE0000; +constexpr NodeId kTestSubjectEmpty = 0x0000000000000000; + +constexpr SubjectId Passcode(PasscodeId passcode) +{ + // For test purposes, stuff passcode into node with tag + return { .node = kTestSubjectPasscode | passcode }; +} + +constexpr SubjectId Node(NodeId node) +{ + return { .node = node }; +} + +constexpr SubjectId CAT1(NodeId node) +{ + return { .node = node }; +} + +constexpr SubjectId CAT2(NodeId node) +{ + return { .node = node }; +} + +constexpr SubjectId Group(GroupId group) +{ + // For test purposes, stuff group into node with tag + return { .node = kTestSubjectGroup | group }; +} + +struct TestTarget +{ + enum Flag + { + kDeviceType = 1 << 0, + kEndpoint = 1 << 1, + kCluster = 1 << 2, + }; + + int flags = kEmptyFlags; + DeviceTypeId deviceType; + EndpointId endpoint; + ClusterId cluster; +}; + +TestTarget Target(EndpointId endpoint, ClusterId cluster) +{ + return { .flags = TestTarget::kEndpoint | TestTarget::kCluster, .endpoint = endpoint, .cluster = cluster }; +} + +TestTarget Target(EndpointId endpoint) +{ + return { .flags = TestTarget::kEndpoint, .endpoint = endpoint }; +} + +TestTarget Target(ClusterId cluster) +{ + return { .flags = TestTarget::kCluster, .cluster = cluster }; +} + +struct TestEntryDelegate +{ + FabricIndex fabricIndex = 0; + AuthMode authMode = AuthMode::kNone; + Privilege privilege = Privilege::kView; + SubjectId subjects[kSubjectsPerEntry + 1]; + TestTarget targets[kTargetsPerEntry + 1]; + const char * tag = ""; + bool touched = false; +}; + +TestEntryDelegate entries[] = { + { + .fabricIndex = 1, + .authMode = AuthMode::kCase, + .privilege = Privilege::kAdminister, + .subjects = { Node(0x1122334455667788) }, + .tag = "1-admin", + }, + { + .fabricIndex = 2, + .authMode = AuthMode::kCase, + .privilege = Privilege::kAdminister, + .subjects = { Node(0x8877665544332211) }, + .tag = "2-admin", + }, + { + .fabricIndex = 2, + .authMode = AuthMode::kCase, + .privilege = Privilege::kView, + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kPase, + .privilege = Privilege::kView, + .subjects = { Passcode(0) }, + .targets = { Target(kEndpoint2, kOnOffCluster) }, + .tag = "1-pase-view-onoff-2", + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kCase, + .privilege = Privilege::kView, + .targets = { Target(kEndpoint1, kLevelControlCluster) }, + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kCase, + .privilege = Privilege::kOperate, + .targets = { Target(kEndpoint1, kColorControlCluster) }, + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kCase, + .privilege = Privilege::kView, + .targets = { Target(kEndpoint2, kOnOffCluster) }, + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kCase, + .privilege = Privilege::kView, + .subjects = { CAT1(86), CAT2(99) }, + .targets = { Target(kEndpoint0), Target(kAccessControlCluster) }, + }, + { + .fabricIndex = 1, + .authMode = AuthMode::kGroup, + .privilege = Privilege::kView, + .subjects = { Group(7) }, + }, + { + .fabricIndex = 3, + .authMode = AuthMode::kCase, + .privilege = Privilege::kAdminister, + }, +}; +constexpr int kNumEntries = sizeof(entries) / sizeof(entries[0]); + +void ResetTouchedEntries() +{ + for (int i = 0; i < kNumEntries; ++i) + { + entries[i].touched = false; + } +} + +bool EntriesTouchedToTag(const char * tag, FabricIndex fabricIndex) +{ + bool expectedTouched = true; + + for (int i = 0; i < kNumEntries; ++i) + { + auto & entry = entries[i]; + if (entry.fabricIndex == fabricIndex) + { + if (entry.touched != expectedTouched) + { + return false; + } + if (strcmp(entry.tag, tag) == 0) + { + expectedTouched = false; + } + } + else if (entry.touched) + { + return false; + } + } + + return true; +} + +class TestEntry : public Entry +{ +public: + virtual ~TestEntry() = default; + + bool MatchesAuthMode(AuthMode authMode) const override { return delegate->authMode == authMode; } + + bool MatchesFabric(FabricIndex fabricIndex) const override + { + return (delegate->fabricIndex == 0) || (delegate->fabricIndex == fabricIndex); + } + + bool MatchesPrivilege(Privilege privilege) const override + { + switch (privilege) + { + case Privilege::kProxyView: + return (delegate->privilege == Privilege::kProxyView) || (delegate->privilege == Privilege::kAdminister); + case Privilege::kView: + if (delegate->privilege == Privilege::kView) + return true; + FALLTHROUGH; // fall through + case Privilege::kOperate: + if (delegate->privilege == Privilege::kOperate) + return true; + FALLTHROUGH; // fall through + case Privilege::kManage: + if (delegate->privilege == Privilege::kManage) + return true; + FALLTHROUGH; // fall through + case Privilege::kAdminister: + return delegate->privilege == Privilege::kAdminister; + } + return false; + } + + bool MatchesSubject(SubjectId subject) const override + { + SubjectId * p = delegate->subjects; + if (p->node == kTestSubjectEmpty) + return true; + for (; p->node != kTestSubjectEmpty; ++p) + { + // Don't call ::MatchesSubject because of special storage/tags + if ((p->node & kTestSubjectMask) == kTestSubjectPasscode) + { + if ((p->node & ~kTestSubjectMask) == subject.passcode) + { + return true; + } + } + else if ((p->node & kTestSubjectMask) == kTestSubjectGroup) + { + if ((p->node & ~kTestSubjectMask) == subject.group) + { + return true; + } + } + else + { + // TODO: handle CASE Authenticated Tags (CAT1/CAT2) + if (p->node == subject.node) + { + return true; + } + } + } + return false; + } + + bool MatchesTarget(EndpointId endpoint, ClusterId cluster) const override + { + TestTarget * p = delegate->targets; + if (p->flags == kEmptyFlags) + return true; + for (; p->flags != kEmptyFlags; ++p) + { + if (((p->flags & TestTarget::kEndpoint) == 0 || p->endpoint == endpoint) && + ((p->flags & TestTarget::kCluster) == 0 || p->cluster == cluster)) + return true; + } + return false; + } + + TestEntryDelegate * delegate; +}; + +class TestEntryIterator : public EntryIterator +{ +public: + virtual ~TestEntryIterator() = default; + + void Initialize() + { + fabricFiltered = false; + entry.delegate = nullptr; + } + + void Initialize(FabricIndex fabricIndex_) + { + fabricFiltered = true; + this->fabricIndex = fabricIndex_; + entry.delegate = nullptr; + } + + Entry * Next() override + { + do + { + if (entry.delegate == nullptr) + { + entry.delegate = entries; + } + else if ((entry.delegate - entries) < kNumEntries) + { + entry.delegate++; + } + } while ((entry.delegate - entries) < kNumEntries && fabricFiltered && entry.delegate->fabricIndex != fabricIndex); + + if ((entry.delegate - entries) < kNumEntries) + { + entry.delegate->touched = true; + return &entry; + } + + return nullptr; + } + + void Release() override {} + + bool fabricFiltered; + FabricIndex fabricIndex; + + TestEntry entry; + TestEntryDelegate * next; +}; + +class TestDataProvider : public AccessControlDataProvider +{ +public: + TestDataProvider() = default; + virtual ~TestDataProvider() = default; + + CHIP_ERROR Init() override { return CHIP_NO_ERROR; } + + void Finish() override {} + + EntryIterator * Entries() const override + { + iterator.Initialize(); + return &iterator; + } + + EntryIterator * Entries(FabricIndex fabricIndex) const override + { + iterator.Initialize(fabricIndex); + return &iterator; + } + + mutable TestEntryIterator iterator; +}; + +TestDataProvider testDataProvider; +AccessControl testAccessControl(testDataProvider); + +void MetaTestIterator(nlTestSuite * inSuite, void * inContext) +{ + EntryIterator * iterator = testDataProvider.Entries(); + NL_TEST_ASSERT(inSuite, iterator != nullptr); + + TestEntryDelegate * p = entries; + while (auto entry = iterator->Next()) + { + NL_TEST_ASSERT_LOOP(inSuite, int(p - entries), static_cast(entry)->delegate == p); + ++p; + } + + NL_TEST_ASSERT(inSuite, p == entries + kNumEntries); + + iterator->Release(); +} + +// Given the entries, test many cases to ensure AccessControl::Check both +// returns the correct answer and has used the expected entries to do so +void TestCheck(nlTestSuite * inSuite, void * inContext) +{ + AccessControl & context = *reinterpret_cast(inContext); + + constexpr struct + { + SubjectDescriptor subjectDescriptor; + RequestPath requestPath; + Privilege privilege; + CHIP_ERROR expectedResult; + const char * expectedTag; + } checks[] = { + // clang-format off + { + { .fabricIndex = 1, .authMode = AuthMode::kCase, .subjects = { Node(0x1122334455667788) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_NO_ERROR, + "1-admin", + }, + { + { .fabricIndex = 1, .authMode = AuthMode::kCase, .subjects = { Node(0x1111222233334444) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_ERROR_ACCESS_DENIED, + "(end)", + }, + { + { .fabricIndex = 1, .authMode = AuthMode::kPase, .subjects = { Passcode(kDefaultCommissioningPasscodeId) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_ERROR_ACCESS_DENIED, + "(end)", + }, + { + { .fabricIndex = 2, .authMode = AuthMode::kCase, .subjects = { Node(0x8877665544332211) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_NO_ERROR, + "2-admin", + }, + { + { .fabricIndex = 2, .authMode = AuthMode::kCase, .subjects = { Node(0x1122334455667788) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_ERROR_ACCESS_DENIED, + "(end)", + }, + { + { .fabricIndex = 2, .authMode = AuthMode::kPase, .subjects = { Passcode(kDefaultCommissioningPasscodeId) } }, + { .endpoint = kEndpoint1, .cluster = kOnOffCluster }, + Privilege::kAdminister, + CHIP_ERROR_ACCESS_DENIED, + "(end)", + }, + { + { .fabricIndex = 1, .authMode = AuthMode::kPase, .subjects = { Passcode(kDefaultCommissioningPasscodeId) } }, + { .endpoint = kEndpoint2, .cluster = kOnOffCluster }, + Privilege::kView, + CHIP_NO_ERROR, + "1-pase-view-onoff-2", + }, + // clang-format on + }; + + for (int i = 0; i < int(sizeof(checks) / sizeof(checks[0])); ++i) + { + auto & check = checks[i]; + ResetTouchedEntries(); + CHIP_ERROR err = context.Check(check.subjectDescriptor, check.requestPath, check.privilege); + NL_TEST_ASSERT_LOOP(inSuite, i, err == check.expectedResult); + NL_TEST_ASSERT_LOOP(inSuite, i, EntriesTouchedToTag(check.expectedTag, check.subjectDescriptor.fabricIndex)); + } +} + +void TestGlobalInstance(nlTestSuite * inSuite, void * inContext) +{ + // Initial instance should not be nullptr + AccessControl * instance = GetAccessControl(); + NL_TEST_ASSERT(inSuite, instance != nullptr); + + // Attempting to set nullptr should have no effect + SetAccessControl(nullptr); + NL_TEST_ASSERT(inSuite, GetAccessControl() == instance); + + // Setting another instance should have immediate effect + SetAccessControl(&testAccessControl); + NL_TEST_ASSERT(inSuite, GetAccessControl() == &testAccessControl); + + // Restoring initial instance should also work + SetAccessControl(instance); + NL_TEST_ASSERT(inSuite, GetAccessControl() == instance); +} + +int Setup(void * inContext) +{ + CHIP_ERROR err = testDataProvider.Init(); + if (err != CHIP_NO_ERROR) + return FAILURE; + err = testAccessControl.Init(); + if (err != CHIP_NO_ERROR) + return FAILURE; + return SUCCESS; +} + +int Teardown(void * inContext) +{ + testDataProvider.Finish(); + testAccessControl.Finish(); + return SUCCESS; +} + +int Initialize(void * inContext) +{ + return SUCCESS; +} + +int Terminate(void * inContext) +{ + return SUCCESS; +} + +} // namespace + +int TestAccessControl() +{ + // clang-format off + constexpr nlTest tests[] = { + NL_TEST_DEF("MetaTestIterator", MetaTestIterator), + NL_TEST_DEF("TestGlobalInstance", TestGlobalInstance), + NL_TEST_DEF("TestCheck", TestCheck), + NL_TEST_SENTINEL() + }; + // clang-format on + + nlTestSuite suite = { + .name = "AccessControl", + .tests = tests, + .setup = Setup, + .tear_down = Teardown, + .initialize = Initialize, + .terminate = Terminate, + }; + + nlTestRunner(&suite, &testAccessControl); + return nlTestRunnerStats(&suite); +} + +CHIP_REGISTER_TEST_SUITE(TestAccessControl); diff --git a/src/lib/core/BUILD.gn b/src/lib/core/BUILD.gn index 4a1fd0522c364a..f8eec50de42863 100644 --- a/src/lib/core/BUILD.gn +++ b/src/lib/core/BUILD.gn @@ -104,6 +104,7 @@ static_library("core") { "DataModelTypes.h", "GroupId.h", "NodeId.h", + "PasscodeId.h", "PeerId.h", ] diff --git a/src/lib/core/PasscodeId.h b/src/lib/core/PasscodeId.h new file mode 100644 index 00000000000000..f49aa19ea26b0e --- /dev/null +++ b/src/lib/core/PasscodeId.h @@ -0,0 +1,30 @@ +/* + * + * Copyright (c) 2021 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. + * 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 + +namespace chip { + +// TODO: Consider making this a class and the various utility methods static +// methods. +using PasscodeId = uint16_t; + +constexpr PasscodeId kDefaultCommissioningPasscodeId = 0; + +} // namespace chip