From f97dcabf8bf74c463d3291d31e9b36fabec0654f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pecka?= <tomas.pecka@cesnet.cz>
Date: Wed, 9 Apr 2025 21:17:19 +0200
Subject: [PATCH 43/44] restconf: allow specifying exec permissions for
 anonymous user access
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires

We want to be able to allow anonymous users to call the RPC
ietf-subscribed-notifications:establish-subscription. Our current NACM
anonymous setup did not allow specifying exec permissions.
After this patch admins are allowed to setup a rule in the anonymous
user rule-list that allows anonymous users to call the
establish-subscriptions RPC. We allow only this one RPC, specifying any
other will result in disabling the anonymous user access.

Change-Id: I5aa6fee2bbdabc0b7deac7fb9afecac6c411aca0
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
 README.md                |  2 +-
 src/auth/Nacm.cpp        | 23 ++++++++++++++++++-----
 tests/restconf-nacm.cpp  | 21 +++++++++++++++++++++
 tests/restconf_utils.cpp |  4 ++++
 4 files changed, 44 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index 646976d..0796dce 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ When certain conditions are met, the anonymous access will be mapped to a NACM a
 There must be some specific access rights set up in `ietf-netconf-acm` model (these are currently very opinionated for our use-case):
 
 1. The first entry of `rule-list` list must be configured for `ANONYMOUS_USER_GROUP`.
-2. All the rules except the last one in this rule-list entry must enable only "read" access operation.
+2. All the rules except the last one in this rule-list entry must enable either only "read" access operation or only "exec" operation on `ietf-subscribed-notifications:establish-subscription` RPC.
 3. The last rule in the first rule-set must be a wildcard rule that disables all operations over all modules.
 
 The anonymous user access is disabled whenever these rules are not met.
diff --git a/src/auth/Nacm.cpp b/src/auth/Nacm.cpp
index d0dc4a2..284fa85 100644
--- a/src/auth/Nacm.cpp
+++ b/src/auth/Nacm.cpp
@@ -9,10 +9,23 @@
 #include "NacmIdentities.h"
 
 namespace {
-bool isRuleReadOnly(const libyang::DataNode& rule)
+bool isWhitelistedRPC(const std::optional<libyang::DataNode>& moduleNameNode, const std::optional<libyang::DataNode>& rpcNameNode)
+{
+    if (!moduleNameNode || !rpcNameNode) {
+        return false;
+    }
+
+    return moduleNameNode->asTerm().valueStr() == "ietf-subscribed-notifications" && rpcNameNode->asTerm().valueStr() == "establish-subscription";
+}
+
+bool isRuleForAnonymousAccess(const libyang::DataNode& rule)
 {
     auto accessOperations = rule.findXPath("access-operations");
-    return accessOperations.size() == 1 && accessOperations.begin()->asTerm().valueStr() == "read";
+    auto rpcName = rule.findPath("rpc-name");
+    return accessOperations.size() == 1 && // Combining access operations is not allowed in anonymous user rules
+        (accessOperations.begin()->asTerm().valueStr() == "read" || // Either read...
+         (accessOperations.begin()->asTerm().valueStr() == "exec" && isWhitelistedRPC(rule.findPath("module-name"), rule.findPath("rpc-name"))) // ...or exec on whitelisted RPC.
+        );
 }
 
 bool isRuleWildcardDeny(const libyang::DataNode& rule)
@@ -25,7 +38,7 @@ bool isRuleWildcardDeny(const libyang::DataNode& rule)
  *
  * The first rule-list element contains rules for anonymous user access, i.e.:
  *  - The group is set to @p anonGroup (this one should contain the anonymous user)
- *  - In rules (except the last one) the access-operation allowed is "read"
+ *  - In rules (except the last one) the access-operation allowed is either "read" or "exec" on a whitelisted RPC.
  *  - The last rule has module-name="*" and action "deny".
  *
  *  @return boolean indicating whether the rules are configured properly for anonymous user access
@@ -60,8 +73,8 @@ bool validAnonymousNacmRules(sysrepo::Session session, const std::string& anonGr
         return false;
     }
 
-    if (!std::all_of(rules.begin(), rules.end() - 1, isRuleReadOnly)) {
-        spdlog::debug("NACM config validation: First n-1 rules in the anonymous rule-list must be configured for read-access only");
+    if (!std::all_of(rules.begin(), rules.end() - 1, isRuleForAnonymousAccess)) {
+        spdlog::debug("NACM config validation: First n-1 rules in the anonymous rule-list must be configured either for read-access only or exec on listed RPC paths");
         return false;
     }
 
diff --git a/tests/restconf-nacm.cpp b/tests/restconf-nacm.cpp
index 30f4210..7799fe8 100644
--- a/tests/restconf-nacm.cpp
+++ b/tests/restconf-nacm.cpp
@@ -113,6 +113,27 @@ TEST_CASE("NACM")
             srSess.applyChanges();
         }
 
+        DOCTEST_SUBCASE("This RPC is not allowed")
+        {
+            srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list");
+            srSess.applyChanges();
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/group[.='yangnobody']", "");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/module-name", "ietf-system");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/action", "permit");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/access-operations", "read");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='1']/path", "/ietf-system:system");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/module-name", "ietf-subscribed-notifications");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/action", "permit");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/access-operations", "read update");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='2']/rpc-name", "/ietf-subscribed-notifications:modify-subscription");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='3']/module-name", "*");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='3']/action", "deny");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", "");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/module-name", "ietf-system");
+            srSess.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/rule[name='1']/action", "permit");
+            srSess.applyChanges();
+        }
+
         DOCTEST_SUBCASE("Anonymous rulelist OK, but not at first place")
         {
             srSess.deleteItem("/ietf-netconf-acm:nacm/rule-list");
diff --git a/tests/restconf_utils.cpp b/tests/restconf_utils.cpp
index 72c2c5a..a137463 100644
--- a/tests/restconf_utils.cpp
+++ b/tests/restconf_utils.cpp
@@ -152,6 +152,10 @@ void setupRealNacm(sysrepo::Session session)
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/action", "permit");
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/access-operations", "read");
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='15']/path", "/example-delete:immutable");
+    session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/module-name", "ietf-subscribed-notifications");
+    session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/action", "permit");
+    session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/access-operations", "exec");
+    session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='16']/rpc-name", "establish-subscription");
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/module-name", "*");
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='anon rule']/rule[name='99']/action", "deny");
     session.setItem("/ietf-netconf-acm:nacm/rule-list[name='dwdm rule']/group[.='optics']", "");
-- 
2.43.0

